<!DOCTYPE html>
<!--
Copyright (c) 2013 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->

<link rel="import" href="/tracing/base/in_memory_trace_stream.html">
<link rel="import" href="/tracing/base/scalar.html">
<link rel="import" href="/tracing/base/time_display_modes.html">
<link rel="import" href="/tracing/base/unit.html">
<link rel="import" href="/tracing/core/test_utils.html">
<link rel="import" href="/tracing/extras/importer/battor_importer.html">
<link rel="import" href="/tracing/extras/importer/trace_event_importer.html">
<link rel="import" href="/tracing/extras/measure/measure.html">
<link rel="import" href="/tracing/extras/v8_config.html">
<link rel="import" href="/tracing/importer/import.html">
<link rel="import" href="/tracing/model/container_memory_dump.html">
<link rel="import" href="/tracing/model/memory_dump_test_utils.html">
<link rel="import" href="/tracing/model/scoped_id.html">
<link rel="import" href="/tracing/model/vm_region.html">

<script>
'use strict';

tr.b.unittest.testSuite(function() {
  const findSliceNamed = tr.c.TestUtils.findSliceNamed;
  const ClockDomainId = tr.model.ClockDomainId;
  const ColorScheme = tr.b.ColorScheme;
  const MeasureAsyncSlice = tr.e.measure.MeasureAsyncSlice;
  const ScopedId = tr.model.ScopedId;
  const VMRegion = tr.model.VMRegion;
  const Scalar = tr.b.Scalar;
  const unitlessNumber_smallerIsBetter =
      tr.b.Unit.byName.unitlessNumber_smallerIsBetter;
  const checkDumpNumericsAndDiagnostics =
      tr.model.MemoryDumpTestUtils.checkDumpNumericsAndDiagnostics;
  const checkVMRegions = tr.model.MemoryDumpTestUtils.checkVMRegions;
  const LIGHT = tr.model.ContainerMemoryDump.LevelOfDetail.LIGHT;
  const DETAILED = tr.model.ContainerMemoryDump.LevelOfDetail.DETAILED;

  function makeModel(events, opt_shift, opt_prune) {
    return tr.c.TestUtils.newModelWithEvents([events], {
      shiftWorldToZero: opt_shift,
      pruneEmptyContainers: opt_prune
    });
  }

  function checkHeapEntry(entry, expectedDump, expectedSize, expectedTitles,
      expectedObjectTypeName, expectedCount) {
    assert.strictEqual(entry.heapDump, expectedDump);
    assert.strictEqual(entry.size, expectedSize);
    assert.strictEqual(entry.objectTypeName, expectedObjectTypeName);
    assert.strictEqual(entry.count, expectedCount);
    if (expectedTitles === undefined) {
      assert.isUndefined(entry.leafStackFrame);
    } else {
      assert.deepEqual(
          entry.leafStackFrame.getUserFriendlyStackTrace(), expectedTitles);
    }
  }

  function getFrame(heapEntry, distance) {
    let frame = heapEntry.leafStackFrame;
    for (; distance > 0; distance--) {
      frame = frame.parentFrame;
    }
    return frame;
  }

  function stringToUint8Array(str) {
    const buffer = new ArrayBuffer(str.length);
    const bufferView = new Uint8Array(buffer);
    for (let i = 0; i < bufferView.length; i++) {
      bufferView[i] = str.charCodeAt(i);
    }
    return bufferView;
  }

  function checkParsedAndStreamInput(events, checkFn, opt_shift, opt_prune) {
    const stream = new tr.b.InMemoryTraceStream(
        stringToUint8Array(JSON.stringify(events)), false);

    checkFn(makeModel(events, opt_shift, opt_prune));
    checkFn(makeModel(stream, opt_shift, opt_prune));
  }

  test('canImportEmpty', function() {
    assert.isFalse(tr.e.importer.TraceEventImporter.canImport([]));
    assert.isFalse(tr.e.importer.TraceEventImporter.canImport(''));
  });

  test('basicSingleThreadNonnestedParsing', function() {
    function checkModel(m) {
      assert.strictEqual(m.numProcesses, 1);
      const p = m.processes[52];
      assert.isDefined(p);

      assert.strictEqual(p.numThreads, 1);
      const t = p.threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.sliceGroup.length, 2);
      assert.strictEqual(t.tid, 53);
      let slice = t.sliceGroup.slices[0];
      assert.strictEqual(slice.title, 'a');
      assert.strictEqual(slice.category, 'foo');
      assert.strictEqual(slice.start, 0);
      assert.closeTo((560 - 520) / 1000, slice.duration, 1e-5);
      assert.strictEqual(slice.subSlices.length, 0);

      slice = t.sliceGroup.slices[1];
      assert.strictEqual(slice.title, 'b');
      assert.strictEqual(slice.category, 'bar');
      assert.closeTo((629 - 520) / 1000, slice.start, 1e-5);
      assert.closeTo((631 - 629) / 1000, slice.duration, 1e-5);
      assert.strictEqual(slice.subSlices.length, 0);
    }

    const events = [
      {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'},
      {name: 'b', args: {}, pid: 52, ts: 629, cat: 'bar', tid: 53, ph: 'B'},
      {name: 'b', args: {}, pid: 52, ts: 631, cat: 'bar', tid: 53, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('nonMonotonicInstantEvents', function() {
    function checkModel(m) {
      assert.strictEqual(m.numProcesses, 1);
      const p = m.processes[52];
      assert.isDefined(p);

      assert.strictEqual(p.numThreads, 3);
      const t1 = p.threads[1];
      assert.isDefined(t1);
      assert.strictEqual(t1.sliceGroup.length, 3);

      let slice = t1.sliceGroup.slices[1];
      assert.strictEqual(slice.title, 'b');
      assert.strictEqual(slice.category, 'foo');
      assert.strictEqual(slice.start, 2);
      assert.strictEqual(slice.duration, 0);
      assert.strictEqual(slice.subSlices.length, 0);

      slice = t1.sliceGroup.slices[2];
      assert.strictEqual(slice.title, 'a');
      assert.strictEqual(slice.category, 'foo');
      assert.strictEqual(slice.start, 3);
      assert.strictEqual(slice.duration, 5);
      assert.strictEqual(slice.subSlices.length, 0);

      const t2 = p.threads[2];
      assert.isDefined(t2);
      assert.strictEqual(t2.sliceGroup.length, 3);

      slice = t2.sliceGroup.slices[1];
      assert.strictEqual(slice.title, 'b');
      assert.strictEqual(slice.start, 2);
      assert.strictEqual(slice.duration, 0);
      assert.strictEqual(slice.subSlices.length, 0);

      slice = t2.sliceGroup.slices[2];
      assert.strictEqual(slice.title, 'a');
      assert.strictEqual(slice.category, 'foo');
      assert.strictEqual(slice.start, 3);
      assert.strictEqual(slice.duration, 5);
      assert.strictEqual(slice.subSlices.length, 0);

      const t3 = p.threads[3];
      assert.isDefined(t3);
      assert.strictEqual(t3.sliceGroup.length, 3);

      slice = t3.sliceGroup.slices[1];
      assert.strictEqual(slice.title, 'b');
      assert.strictEqual(slice.start, 2);
      assert.strictEqual(slice.duration, 0);
      assert.strictEqual(slice.subSlices.length, 0);

      slice = t3.sliceGroup.slices[2];
      assert.strictEqual(slice.title, 'a');
      assert.strictEqual(slice.category, 'foo');
      assert.strictEqual(slice.start, 3);
      assert.strictEqual(slice.duration, 5);
      assert.strictEqual(slice.subSlices.length, 0);
    }

    const events = [
      {name: 'x', args: {}, pid: 52, ts: 0, cat: 'foo', tid: 1, ph: 'R'},
      {name: 'a', args: {}, pid: 52, ts: 3000, cat: 'foo', tid: 1, ph: 'X',
        dur: 5000},
      {name: 'b', args: {}, pid: 52, ts: 2000, cat: 'foo', tid: 1, ph: 'R'},

      {name: 'x', args: {}, pid: 52, ts: 0, cat: 'foo', tid: 2, ph: 'R'},
      {name: 'b', args: {}, pid: 52, ts: 2000, cat: 'foo', tid: 2, ph: 'R'},
      {name: 'a', args: {}, pid: 52, ts: 3000, cat: 'foo', tid: 2, ph: 'X',
        dur: 5000},

      {name: 'x', args: {}, pid: 52, ts: 0, cat: 'foo', tid: 3, ph: 'R'},
      {name: 'a', args: {}, pid: 52, ts: 3000, cat: 'foo', tid: 3, ph: 'B'},
      {name: 'b', args: {}, pid: 52, ts: 2000, cat: 'foo', tid: 3, ph: 'R'},
      {name: 'a', args: {}, pid: 52, ts: 8000, cat: 'foo', tid: 3, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('basicSingleThreadNonnestedParsingWithCpuDuration', function() {
    function checkModel(m) {
      assert.strictEqual(m.numProcesses, 1);
      const p = m.processes[52];
      assert.isDefined(p);

      assert.strictEqual(p.numThreads, 1);
      const t = p.threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.sliceGroup.length, 2);
      assert.strictEqual(t.tid, 53);
      let slice = t.sliceGroup.slices[0];
      assert.strictEqual(slice.title, 'a');
      assert.strictEqual(slice.category, 'foo');
      assert.strictEqual(slice.start, 0);
      assert.closeTo((560 - 520) / 1000, slice.duration, 1e-5);
      assert.closeTo((259 - 221) / 1000, slice.cpuDuration, 1e-5);
      assert.strictEqual(slice.subSlices.length, 0);

      slice = t.sliceGroup.slices[1];
      assert.strictEqual(slice.title, 'b');
      assert.strictEqual(slice.category, 'bar');
      assert.closeTo((629 - 520) / 1000, slice.start, 1e-5);
      assert.closeTo((631 - 629) / 1000, slice.duration, 1e-5);
      assert.closeTo((331 - 329) / 1000, slice.cpuDuration, 1e-5);
      assert.strictEqual(slice.subSlices.length, 0);
    }

    const events = [
      {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B', tts: 221}, // @suppress longLineCheck
      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E', tts: 259}, // @suppress longLineCheck
      {name: 'b', args: {}, pid: 52, ts: 629, cat: 'bar', tid: 53, ph: 'B', tts: 329}, // @suppress longLineCheck
      {name: 'b', args: {}, pid: 52, ts: 631, cat: 'bar', tid: 53, ph: 'E', tts: 331}  // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('argumentDupeCreatesNonFailingImportError', function() {
    function checkModel(m) {
      const t = m.processes[1].threads[1];
      const sA = findSliceNamed(t.sliceGroup, 'a');

      assert.strictEqual(sA.args.x, 2);
      assert.isTrue(m.hasImportWarnings);
      assert.strictEqual(1, m.importWarnings.length);
    }

    const events = [
      {name: 'a',
        args: {'x': 1},
        pid: 1,
        ts: 520,
        cat: 'foo',
        tid: 1,
        ph: 'B'},
      {name: 'a',
        args: {'x': 2},
        pid: 1,
        ts: 560,
        cat: 'foo',
        tid: 1,
        ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importMissingArgs', function() {
    const events = [
      {name: 'a', pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
      {name: 'a', pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'},
      {name: 'b', pid: 52, ts: 629, cat: 'bar', tid: 53, ph: 'I'}
    ];

    // This should not throw an exception.
    checkParsedAndStreamInput(events, m => {});
  });

  test('importDoesNotChokeOnNulls', function() {
    const events = [
      {name: 'a', args: { foo: null }, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'}, // @suppress longLineCheck
      {name: 'a', pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
    ];

    // This should not throw an exception.
    checkParsedAndStreamInput(events, m => {});
  });

  test('categoryBeginEndMismatchPrefersBegin', function() {
    function checkModel(m) {
      assert.strictEqual(m.numProcesses, 1);
      const p = m.processes[52];
      assert.isDefined(p);

      assert.strictEqual(p.numThreads, 1);
      const t = p.threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.sliceGroup.length, 1);
      assert.strictEqual(t.tid, 53);
      const slice = t.sliceGroup.slices[0];
      assert.strictEqual(slice.title, 'a');
      assert.strictEqual(slice.category, 'foo');
    }

    const events = [
      {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'bar', tid: 53, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('beginEndNameMismatch', function() {
    function checkModel(m) {
      assert.isTrue(m.hasImportWarnings);
      assert.strictEqual(m.importWarnings.length, 1);
    }

    const events = [
      {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
      {name: 'b', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('nestedParsing', function() {
    function checkModel(m) {
      const t = m.processes[1].threads[1];

      const sA = findSliceNamed(t.sliceGroup, 'a');
      const sB = findSliceNamed(t.sliceGroup, 'b');

      assert.strictEqual(sA.title, 'a');
      assert.strictEqual(sA.category, 'foo');
      assert.strictEqual(sA.start, 0.001);
      assert.strictEqual(sA.duration, 0.003);
      assert.strictEqual(sA.selfTime, 0.002);
      assert.strictEqual(sA.cpuSelfTime, 0.001);

      assert.strictEqual(sB.title, 'b');
      assert.strictEqual(sB.category, 'bar');
      assert.strictEqual(sB.start, 0.002);
      assert.strictEqual(sB.duration, 0.001);

      assert.strictEqual(1, sA.subSlices.length);
      assert.strictEqual(sB, sA.subSlices[0]);
      assert.strictEqual(sA, sB.parentSlice);
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, tts: 1, cat: 'foo', tid: 1, ph: 'B'},
      {name: 'b', args: {}, pid: 1, ts: 2, tts: 2, cat: 'bar', tid: 1, ph: 'B'},
      {name: 'b', args: {}, pid: 1, ts: 3, tts: 3, cat: 'bar', tid: 1, ph: 'E'},
      {name: 'a', args: {}, pid: 1, ts: 4, tts: 3, cat: 'foo', tid: 1, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('nestedParsingWithTwoSubSlices', function() {
    function checkModel(m) {
      const t = m.processes[1].threads[1];

      const sA = findSliceNamed(t.sliceGroup, 'a');
      const sB = findSliceNamed(t.sliceGroup, 'b');
      const sC = findSliceNamed(t.sliceGroup, 'c');

      assert.strictEqual(sA.title, 'a');
      assert.strictEqual(sA.category, 'foo');
      assert.strictEqual(sA.start, 0.001);
      assert.strictEqual(sA.duration, 0.007);
      assert.strictEqual(sA.selfTime, 0.004);
      assert.strictEqual(sA.cpuSelfTime, 0.005);

      assert.strictEqual(sB.title, 'b');
      assert.strictEqual(sB.category, 'bar');
      assert.strictEqual(sB.start, 0.002);
      assert.strictEqual(sB.duration, 0.001);

      assert.strictEqual(sC.title, 'c');
      assert.strictEqual(sC.category, 'baz');
      assert.strictEqual(sC.start, 0.005);
      assert.strictEqual(sC.duration, 0.002);

      assert.strictEqual(sA.subSlices.length, 2);
      assert.strictEqual(sA.subSlices[0], sB);
      assert.strictEqual(sA.subSlices[1], sC);
      assert.strictEqual(sB.parentSlice, sA);
      assert.strictEqual(sC.parentSlice, sA);
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, tts: 1, cat: 'foo', tid: 1, ph: 'B'},
      {name: 'b', args: {}, pid: 1, ts: 2, tts: 2, cat: 'bar', tid: 1, ph: 'B'},
      {name: 'b', args: {}, pid: 1, ts: 3, tts: 3, cat: 'bar', tid: 1, ph: 'E'},
      {name: 'c', args: {}, pid: 1, ts: 5, tts: 5, cat: 'baz', tid: 1, ph: 'B'},
      {name: 'c', args: {}, pid: 1, ts: 7, tts: 6, cat: 'baz', tid: 1, ph: 'E'},
      {name: 'a', args: {}, pid: 1, ts: 8, tts: 8, cat: 'foo', tid: 1, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('nestedParsingWithDoubleNesting', function() {
    function checkModel(m) {
      const t = m.processes[1].threads[1];

      const sA = findSliceNamed(t.sliceGroup, 'a');
      const sB = findSliceNamed(t.sliceGroup, 'b');
      const sC = findSliceNamed(t.sliceGroup, 'c');

      assert.strictEqual(sA.title, 'a');
      assert.strictEqual(sA.category, 'foo');
      assert.strictEqual(sA.start, 0.001);
      assert.strictEqual(sA.duration, 0.007);
      assert.strictEqual(sA.selfTime, 0.002);

      assert.strictEqual(sB.title, 'b');
      assert.strictEqual(sB.category, 'bar');
      assert.strictEqual(sB.start, 0.002);
      assert.strictEqual(sB.duration, 0.005);
      assert.strictEqual(sA.selfTime, 0.002);

      assert.strictEqual(sC.title, 'c');
      assert.strictEqual(sC.category, 'baz');
      assert.strictEqual(sC.start, 0.003);
      assert.strictEqual(sC.duration, 0.002);

      assert.strictEqual(sA.subSlices.length, 1);
      assert.strictEqual(sA.subSlices[0], sB);
      assert.strictEqual(sB.parentSlice, sA);

      assert.strictEqual(sB.subSlices.length, 1);
      assert.strictEqual(sB.subSlices[0], sC);
      assert.strictEqual(sC.parentSlice, sB);
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
      {name: 'b', args: {}, pid: 1, ts: 2, cat: 'bar', tid: 1, ph: 'B'},
      {name: 'c', args: {}, pid: 1, ts: 3, cat: 'baz', tid: 1, ph: 'B'},
      {name: 'c', args: {}, pid: 1, ts: 5, cat: 'baz', tid: 1, ph: 'E'},
      {name: 'b', args: {}, pid: 1, ts: 7, cat: 'bar', tid: 1, ph: 'E'},
      {name: 'a', args: {}, pid: 1, ts: 8, cat: 'foo', tid: 1, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });


  test('autoclosing', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t1 = p.threads[1];
      const t2 = p.threads[2];
      assert.isTrue(t1.sliceGroup.slices[0].didNotFinish);
      assert.strictEqual(t1.sliceGroup.slices[0].end, 0.001);
    }

    const events = [
      // Slice that doesn't finish.
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},

      // Slice that does finish to give an 'end time' to make autoclosing work.
      {name: 'b', args: {}, pid: 1, ts: 1, cat: 'bar', tid: 2, ph: 'B'},
      {name: 'b', args: {}, pid: 1, ts: 2, cat: 'bar', tid: 2, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('autoclosingLoneBegin', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[1];
      const slice = t.sliceGroup.slices[0];
      assert.strictEqual(slice.title, 'a');
      assert.strictEqual(slice.category, 'foo');
      assert.isTrue(slice.didNotFinish);
      assert.strictEqual(slice.start, 0);
      assert.strictEqual(slice.duration, 0);
    }

    const events = [
      // Slice that doesn't finish.
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('autoclosingWithSubTasks', function() {
    function checkModel(m) {
      const t = m.processes[1].threads[1];

      const sA = findSliceNamed(t.sliceGroup, 'a');
      const sB1 = findSliceNamed(t.sliceGroup, 'b1');
      const sB2 = findSliceNamed(t.sliceGroup, 'b2');

      assert.strictEqual(sA.end, 0.003);
      assert.strictEqual(sB1.end, 0.003);
      assert.strictEqual(sB2.end, 0.003);
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
      {name: 'b1', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'B'},
      {name: 'b1', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'E'},
      {name: 'b2', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'B'}
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('autoclosingWithEventsOutsideBounds', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[1];
      assert.strictEqual(t.sliceGroup.length, 2);

      const slice = findSliceNamed(t.sliceGroup, 'a');
      assert.strictEqual(slice.title, 'a');
      assert.strictEqual(slice.category, 'foo');
      assert.strictEqual(slice.start, 0);
      assert.strictEqual(slice.duration, 0.003);

      const t2 = p.threads[2];
      const slice2 = findSliceNamed(t2.sliceGroup, 'c');
      assert.strictEqual(slice2.title, 'c');
      assert.strictEqual(slice2.category, 'bar');
      assert.strictEqual(slice2.start, 0.001);
      assert.strictEqual(slice2.duration, 0.001);

      assert.strictEqual(m.bounds.min, 0.000);
      assert.strictEqual(m.bounds.max, 0.003);
    }

    const events = [
      // Slice that begins before min and ends after max of the other threads.
      {name: 'a', args: {}, pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'B'},
      {name: 'b', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'B'},

      // Slice that does finish to give an 'end time' to establish a basis
      {name: 'c', args: {}, pid: 1, ts: 1, cat: 'bar', tid: 2, ph: 'B'},
      {name: 'c', args: {}, pid: 1, ts: 2, cat: 'bar', tid: 2, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('nestedAutoclosing', function() {
    function checkModel(m) {
      const t1 = m.processes[1].threads[1];
      const t2 = m.processes[1].threads[2];

      const sA1 = findSliceNamed(t1.sliceGroup, 'a1');
      const sA2 = findSliceNamed(t1.sliceGroup, 'a2');
      const sB = findSliceNamed(t2.sliceGroup, 'b');

      assert.strictEqual(sA1.end, 0.0015);
      assert.strictEqual(sA2.end, 0.0015);
    }

    const events = [
      // Tasks that don't finish.
      {name: 'a1', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
      {name: 'a2', args: {}, pid: 1, ts: 1.5, cat: 'foo', tid: 1, ph: 'B'},

      // Slice that does finish to give an 'end time' to make autoclosing work.
      {name: 'b', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 2, ph: 'B'},
      {name: 'b', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 2, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('taskColoring', function() {
    // The test below depends on hashing of 'a' !== 'b'. Fail early if that
    // assumption is incorrect.
    assert.notEqual(ColorScheme.getStringHash('a'),
        ColorScheme.getStringHash('b'));

    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[1];
      const a1 = t.sliceGroup.slices[0];
      assert.strictEqual(a1.title, 'a');
      assert.strictEqual(a1.category, 'foo');
      const b = t.sliceGroup.slices[1];
      assert.strictEqual(b.title, 'b');
      assert.strictEqual(b.category, 'bar');
      assert.notEqual(b.colorId, a1.colorId);
      const a2 = t.sliceGroup.slices[2];
      assert.strictEqual(a2.title, 'a');
      assert.strictEqual(a2.category, 'baz');
      assert.strictEqual(a1.colorId, a2.colorId);
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
      {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
      {name: 'b', args: {}, pid: 1, ts: 3, cat: 'bar', tid: 1, ph: 'B'},
      {name: 'b', args: {}, pid: 1, ts: 4, cat: 'bar', tid: 1, ph: 'E'},
      {name: 'a', args: {}, pid: 1, ts: 5, cat: 'baz', tid: 1, ph: 'B'},
      {name: 'a', args: {}, pid: 1, ts: 6, cat: 'baz', tid: 1, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('durationColorArgument', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[1];
      assert.strictEqual(t.sliceGroup.slices[0].colorId,
          ColorScheme.getColorIdForReservedName('thread_state_unknown'));
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B', cname: 'thread_state_unknown'}, // @suppress longLineCheck
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'E', cname: 'thread_state_unknown'} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('durationColorEnd', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[1];
      assert.strictEqual(t.sliceGroup.slices[0].colorId,
          ColorScheme.getColorIdForReservedName('thread_state_unknown'));
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B', cname: 'thread_state_sleeping'}, // @suppress longLineCheck
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'E', cname: 'thread_state_unknown'} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('completeColorArgument', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[1];
      assert.strictEqual(t.sliceGroup.slices[0].colorId,
          ColorScheme.getColorIdForReservedName('generic_work'));
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, dur: 1, cat: 'foo', tid: 1, ph: 'X', cname: 'generic_work'} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('asyncColorArgument', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[1];
      assert.strictEqual(t.asyncSliceGroup.slices[0].colorId,
          ColorScheme.getColorIdForReservedName('generic_work'));
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'b', id: 1, cname: 'generic_work'}, // @suppress longLineCheck
      {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'e', id: 1, cname: 'generic_work'} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('asyncColorEnd', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[1];
      assert.strictEqual(t.asyncSliceGroup.slices[0].colorId,
          ColorScheme.getColorIdForReservedName('generic_work'));
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'b', id: 1, cname: 'thread_state_unknown'}, // @suppress longLineCheck
      {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'e', id: 1, cname: 'generic_work'} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('instantThreadColorArgument', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[1];
      assert.strictEqual(t.sliceGroup.slices[0].colorId,
          ColorScheme.getColorIdForReservedName('generic_work'));
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'I', id: 1, cname: 'generic_work'} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('instantProcessColorArgument', function() {
    function checkModel(m) {
      const p = m.processes[1];
      assert.strictEqual(p.instantEvents[0].colorId,
          ColorScheme.getColorIdForReservedName('generic_work'));
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'I', id: 1, s: 'p', cname: 'generic_work'} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('counterColorArgument', function() {
    function checkModel(m) {
      const p = m.processes[1];
      assert.strictEqual(p.counters['foo.a[1]'].series[0].color,
          ColorScheme.getColorIdForReservedName('generic_work'));
      assert.strictEqual(p.counters['foo.a[1]'].series.length, 1);
    }

    const events = [
      {name: 'a', args: {'cats': 10}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'C', id: 1, cname: 'generic_work'} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('objectColorArgument', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const i = p.objects.instanceMapsByScopedId_.ptr[1].instances[0];
      assert.strictEqual(i.colorId,
          ColorScheme.getColorIdForReservedName('generic_work'));
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'N', id: 1, cname: 'generic_work'}, // @suppress longLineCheck
      {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'O', id: 1, cname: 'generic_work'}, // @suppress longLineCheck
      {name: 'a', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'D', id: 1, cname: 'generic_work'} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('objectColorEnd', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const i = p.objects.instanceMapsByScopedId_.ptr[1].instances[0];
      assert.strictEqual(i.colorId,
          ColorScheme.getColorIdForReservedName('generic_work'));
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'N', id: 1, cname: 'thread_state_sleeping'}, // @suppress longLineCheck
      {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'O', id: 1, cname: 'thread_state_unknown'}, // @suppress longLineCheck
      {name: 'a', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'D', id: 1, cname: 'generic_work'} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('multipleThreadParsing', function() {
    function checkModel(m) {
      assert.strictEqual(m.numProcesses, 1);
      const p = m.processes[1];
      assert.isDefined(p);

      assert.strictEqual(p.numThreads, 2);

      // Check thread 1.
      let t = p.threads[1];
      assert.isDefined(t);
      assert.strictEqual(t.sliceGroup.length, 1);
      assert.strictEqual(t.tid, 1);

      let slice = t.sliceGroup.slices[0];
      assert.strictEqual(slice.title, 'a');
      assert.strictEqual(slice.category, 'foo');
      assert.strictEqual(slice.start, 0);
      assert.strictEqual(slice.duration, (2 - 1) / 1000);
      assert.strictEqual(slice.subSlices.length, 0);

      // Check thread 2.
      t = p.threads[2];
      assert.isDefined(t);
      assert.strictEqual(t.sliceGroup.length, 1);
      assert.strictEqual(t.tid, 2);

      slice = t.sliceGroup.slices[0];
      assert.strictEqual(slice.title, 'b');
      assert.strictEqual(slice.category, 'bar');
      assert.strictEqual(slice.start, (3 - 1) / 1000);
      assert.strictEqual(slice.duration, (4 - 3) / 1000);
      assert.strictEqual(slice.subSlices.length, 0);
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
      {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
      {name: 'b', args: {}, pid: 1, ts: 3, cat: 'bar', tid: 2, ph: 'B'},
      {name: 'b', args: {}, pid: 1, ts: 4, cat: 'bar', tid: 2, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('multiplePidParsing', function() {
    function checkModel(m) {
      assert.strictEqual(m.numProcesses, 2);
      let p = m.processes[1];
      assert.isDefined(p);

      assert.strictEqual(p.numThreads, 1);

      // Check process 1 thread 1.
      let t = p.threads[1];
      assert.isDefined(t);
      assert.strictEqual(t.sliceGroup.length, 1);
      assert.strictEqual(t.tid, 1);

      let slice = t.sliceGroup.slices[0];
      assert.strictEqual(slice.title, 'a');
      assert.strictEqual(slice.category, 'foo');
      assert.strictEqual(slice.start, 0);
      assert.strictEqual(slice.duration, (2 - 1) / 1000);
      assert.strictEqual(slice.subSlices.length, 0);

      // Check process 2 thread 2.
      p = m.processes[2];
      assert.isDefined(p);
      assert.strictEqual(p.numThreads, 1);
      t = p.threads[2];
      assert.isDefined(t);
      assert.strictEqual(t.sliceGroup.length, 1);
      assert.strictEqual(t.tid, 2);

      slice = t.sliceGroup.slices[0];
      assert.strictEqual(slice.title, 'b');
      assert.strictEqual(slice.category, 'bar');
      assert.strictEqual(slice.start, (3 - 1) / 1000);
      assert.strictEqual(slice.duration, (4 - 3) / 1000);
      assert.strictEqual(slice.subSlices.length, 0);

      // Check getAllThreads.
      assert.deepEqual(m.getAllThreads(),
          [m.processes[1].threads[1], m.processes[2].threads[2]]);
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
      {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
      {name: 'b', args: {}, pid: 2, ts: 3, cat: 'bar', tid: 2, ph: 'B'},
      {name: 'b', args: {}, pid: 2, ts: 4, cat: 'bar', tid: 2, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  // Process names.
  test('processNames', function() {
    function checkModel(m) {
      assert.strictEqual(m.processes[1].name, 'SomeProcessName');
    }

    const events = [
      {name: 'process_name', args: {name: 'SomeProcessName'},
        pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'M'},
      {name: 'process_name', args: {name: 'SomeProcessName'},
        pid: 2, ts: 0, cat: 'foo', tid: 1, ph: 'M'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  // Process labels.
  test('processLabels', function() {
    function checkModel(m) {
      assert.deepEqual(m.processes[1].labels, ['foo', 'bar', 'baz']);
      assert.deepEqual(m.processes[2].labels, ['baz']);
    }

    const events = [
      {name: 'process_labels', args: {labels: 'foo,bar,bar,foo,baz'},
        pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'M'},
      {name: 'process_labels', args: {labels: 'baz'},
        pid: 2, ts: 0, cat: 'foo', tid: 1, ph: 'M'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  // Process uptime.
  test('processUptime', function() {
    const events = [
      {name: 'process_uptime_seconds', args: {uptime: 10},
        pid: 1, ts: 0, tid: 1, ph: 'M'},
      {name: 'process_uptime_seconds', args: {uptime: 20},
        pid: 2, ts: 0, tid: 1, ph: 'M'}
    ];
    const m = makeModel(events);
    assert.strictEqual(m.processes[1].uptime_seconds, 10);
    assert.strictEqual(m.processes[2].uptime_seconds, 20);
  });

  // Process sort index.
  test('processSortIndex', function() {
    function checkModel(m) {
      // By name, p1 is before p2. But, its sort index overrides that.
      assert.isAbove(m.processes[1].compareTo(m.processes[2]), 0);
    }

    const events = [
      {name: 'process_name', args: {name: 'First'},
        pid: 2, ts: 0, cat: 'foo', tid: 1, ph: 'M'},
      {name: 'process_name', args: {name: 'Second'},
        pid: 2, ts: 0, cat: 'foo', tid: 1, ph: 'M'},
      {name: 'process_sort_index', args: {sort_index: 1},
        pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'M'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  // Thread names.
  test('threadNames', function() {
    function checkModel(m) {
      assert.strictEqual(m.processes[1].threads[1].name, 'Thread 1');
      assert.strictEqual(m.processes[2].threads[2].name, 'Thread 2');
    }

    const events = [
      {name: 'thread_name', args: {name: 'Thread 1'},
        pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'M'},
      {name: 'thread_name', args: {name: 'Thread 2'},
        pid: 2, ts: 0, cat: 'foo', tid: 2, ph: 'M'}
    ];
    checkParsedAndStreamInput(events, checkModel, false, false);
  });

  // Thread sort index.
  test('threadSortIndex', function() {
    function checkModel(m) {
      // By name, t1 is before t2. But, its sort index overrides that.
      const t1 = m.processes[1].threads[1];
      const t2 = m.processes[1].threads[2];
      assert.isAbove(t1.compareTo(t2), 0);
    }

    const events = [
      {name: 'thread_name', args: {name: 'Thread 1'},
        pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'M'},
      {name: 'thread_name', args: {name: 'Thread 2'},
        pid: 1, ts: 0, cat: 'foo', tid: 2, ph: 'M'},
      {name: 'thread_sort_index', args: {sort_index: 1},
        pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'M'}
    ];
    checkParsedAndStreamInput(events, checkModel, false, false);
  });

  // CPU counts.
  test('cpuCounts', function() {
    function checkModel(m) {
      assert.strictEqual(m.kernel.softwareMeasuredCpuCount, 4);
      assert.strictEqual(m.kernel.bestGuessAtCpuCount, 4);
    }

    const events = [
      {name: 'num_cpus', args: {number: 4},
        pid: 7, ts: 0, cat: 'foo', tid: 0, ph: 'M'},
      {name: 'num_cpus', args: {number: 4},
        pid: 14, ts: 0, cat: 'foo', tid: 0, ph: 'M'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('cpuCountsWithSandboxBeingConfused', function() {
    function checkModel(m) {
      assert.strictEqual(m.kernel.softwareMeasuredCpuCount, 4);
      assert.strictEqual(m.kernel.bestGuessAtCpuCount, 4);
    }

    const events = [
      {name: 'num_cpus', args: {number: 4},
        pid: 7, ts: 0, cat: 'foo', tid: 0, ph: 'M'},
      {name: 'num_cpus', args: {number: 1},
        pid: 14, ts: 0, cat: 'foo', tid: 0, ph: 'M'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('parsingWhenEndComesFirst', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[1];
      assert.strictEqual(t.sliceGroup.length, 1);
      assert.strictEqual(t.sliceGroup.slices[0].title, 'a');
      assert.strictEqual(t.sliceGroup.slices[0].category, 'foo');
      assert.strictEqual(t.sliceGroup.slices[0].start, 0.004);
      assert.strictEqual(t.sliceGroup.slices[0].duration, 0.001);
      assert.isTrue(m.hasImportWarnings);
      assert.strictEqual(m.importWarnings.length, 1);
    }

    const events = [
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'E'},
      {name: 'a', args: {}, pid: 1, ts: 4, cat: 'foo', tid: 1, ph: 'B'},
      {name: 'a', args: {}, pid: 1, ts: 5, cat: 'foo', tid: 1, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('immediateParsing', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[1];

      assert.strictEqual(t.sliceGroup.length, 3);
      assert.strictEqual(t.sliceGroup.slices[0].start, 0.001);
      assert.strictEqual(t.sliceGroup.slices[0].duration, 0.003);
      assert.strictEqual(t.sliceGroup.slices[1].start, 0.002);
      assert.strictEqual(t.sliceGroup.slices[1].duration, 0);
      assert.strictEqual(t.sliceGroup.slices[2].start, 0.004);

      const slice = findSliceNamed(t.sliceGroup, 'a');
      assert.strictEqual(slice.title, 'a');
      assert.strictEqual(slice.category, 'foo');
      assert.strictEqual(slice.duration, 0.003);

      const immed = findSliceNamed(t.sliceGroup, 'immediate');
      assert.strictEqual(immed.title, 'immediate');
      assert.strictEqual(immed.category, 'bar');
      assert.strictEqual(immed.start, 0.002);
      assert.strictEqual(immed.duration, 0);

      const slower = findSliceNamed(t.sliceGroup, 'slower');
      assert.strictEqual(slower.title, 'slower');
      assert.strictEqual(slower.category, 'baz');
      assert.strictEqual(slower.start, 0.004);
      assert.strictEqual(slower.duration, 0);
    }

    const events = [
      // Need to include immediates inside a task so the timeline
      // recentering/zeroing doesn't clobber their timestamp.
      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
      {name: 'immediate', args: {}, pid: 1, ts: 2, cat: 'bar', tid: 1, ph: 'I'},
      {name: 'slower', args: {}, pid: 1, ts: 4, cat: 'baz', tid: 1, ph: 'i'},
      {name: 'a', args: {}, pid: 1, ts: 4, cat: 'foo', tid: 1, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('simpleCounter', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const ctr = m.processes[1].counters['foo.ctr'];

      assert.strictEqual(ctr.name, 'ctr');
      assert.strictEqual(ctr.category, 'foo');
      assert.strictEqual(ctr.numSamples, 3);
      assert.strictEqual(ctr.numSeries, 1);

      assert.strictEqual(ctr.series[0].name, 'value');
      assert.strictEqual(ctr.series[0].color,
          ColorScheme.getColorIdForGeneralPurposeString('ctr.value'));

      assert.deepEqual(ctr.timestamps, [0, 0.01, 0.02]);

      const samples = [];
      ctr.series[0].samples.forEach(function(sample) {
        samples.push(sample.value);
      });
      assert.deepEqual(samples, [0, 10, 0]);

      assert.deepEqual(ctr.totals, [0, 10, 0]);
      assert.strictEqual(ctr.maxTotal, 10);
    }

    const events = [
      {name: 'ctr', args: {'value': 0}, pid: 1, ts: 0, cat: 'foo', tid: 1,
        ph: 'C'},
      {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
        ph: 'C'},
      {name: 'ctr', args: {'value': 0}, pid: 1, ts: 20, cat: 'foo', tid: 1,
        ph: 'C'}

    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('instanceCounter', function() {
    function checkModel(m) {
      const p = m.processes[1];
      let ctr = m.processes[1].counters['foo.ctr[0]'];
      assert.strictEqual(ctr.name, 'ctr[0]');
      assert.strictEqual(ctr.category, 'foo');
      assert.strictEqual(ctr.numSamples, 2);
      assert.strictEqual(ctr.numSeries, 1);

      assert.deepEqual(ctr.timestamps, [0, 0.01]);
      let samples = [];
      ctr.series[0].samples.forEach(function(sample) {
        samples.push(sample.value);
      });
      assert.deepEqual(samples, [0, 10]);

      ctr = m.processes[1].counters['foo.ctr[1]'];
      assert.strictEqual(ctr.name, 'ctr[1]');
      assert.strictEqual(ctr.category, 'foo');
      assert.strictEqual(ctr.numSamples, 3);
      assert.strictEqual(ctr.numSeries, 1);
      assert.deepEqual(ctr.timestamps, [0.01, 0.015, 0.02]);

      samples = [];
      ctr.series[0].samples.forEach(function(sample) {
        samples.push(sample.value);
      });
      assert.deepEqual(samples, [10, 20, 30]);

      ctr = m.processes[1].counters['bar.ctr[2]'];
      assert.strictEqual(ctr.name, 'ctr[2]');
      assert.strictEqual(ctr.category, 'bar');
      assert.strictEqual(ctr.numSamples, 1);
      assert.strictEqual(ctr.numSeries, 1);
      assert.deepEqual(ctr.timestamps, [0.025]);
      samples = [];
      ctr.series[0].samples.forEach(function(sample) {
        samples.push(sample.value);
      });
      assert.deepEqual(samples, [40]);
    }

    const events = [
      {name: 'ctr', args: {'value': 0}, pid: 1, ts: 0, cat: 'foo', tid: 1,
        ph: 'C', id: 0},
      {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
        ph: 'C', id: 0},
      {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
        ph: 'C', id: 1},
      {name: 'ctr', args: {'value': 20}, pid: 1, ts: 15, cat: 'foo', tid: 1,
        ph: 'C', id: 1},
      {name: 'ctr', args: {'value': 30}, pid: 1, ts: 20, cat: 'foo', tid: 1,
        ph: 'C', id: 1},
      {name: 'ctr', args: {'value': 40}, pid: 1, ts: 25, cat: 'bar', tid: 1,
        ph: 'C', id: 2}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('multiCounterUpdateBounds', function() {
    const ctr = new tr.model.Counter(undefined, 'testBasicCounter',
        '', 'testBasicCounter');
    const value1Series = new tr.model.CounterSeries(
        'value1', 'testBasicCounter.value1');
    const value2Series = new tr.model.CounterSeries(
        'value2', 'testBasicCounter.value2');
    ctr.addSeries(value1Series);
    ctr.addSeries(value2Series);

    value1Series.addCounterSample(0, 0);
    value1Series.addCounterSample(1, 1);
    value1Series.addCounterSample(2, 1);
    value1Series.addCounterSample(3, 2);
    value1Series.addCounterSample(4, 3);
    value1Series.addCounterSample(5, 1);
    value1Series.addCounterSample(6, 3);
    value1Series.addCounterSample(7, 3.1);

    value2Series.addCounterSample(0, 0);
    value2Series.addCounterSample(1, 0);
    value2Series.addCounterSample(2, 1);
    value2Series.addCounterSample(3, 1.1);
    value2Series.addCounterSample(4, 0);
    value2Series.addCounterSample(5, 7);
    value2Series.addCounterSample(6, 0);
    value2Series.addCounterSample(7, 0.5);

    ctr.updateBounds();

    assert.strictEqual(ctr.bounds.min, 0);
    assert.strictEqual(ctr.bounds.max, 7);
    assert.strictEqual(ctr.maxTotal, 8);
    assert.deepEqual([0, 0,
      1, 1,
      1, 2,
      2, 3.1,
      3, 3,
      1, 8,
      3, 3,
      3.1, 3.6], ctr.totals);
  });

  test('multiCounter', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const ctr = m.processes[1].counters['foo.ctr'];
      assert.strictEqual(ctr.name, 'ctr');

      assert.strictEqual(ctr.name, 'ctr');
      assert.strictEqual(ctr.category, 'foo');
      assert.strictEqual(ctr.numSamples, 3);
      assert.strictEqual(ctr.numSeries, 2);

      assert.strictEqual(ctr.series[0].name, 'value1');
      assert.strictEqual(ctr.series[1].name, 'value2');
      assert.strictEqual(ctr.series[0].color,
          ColorScheme.getColorIdForGeneralPurposeString('ctr.value1'));
      assert.strictEqual(ctr.series[1].color,
          ColorScheme.getColorIdForGeneralPurposeString('ctr.value2'));

      assert.deepEqual(ctr.timestamps, [0, 0.01, 0.02]);
      const samples = [];
      ctr.series[0].samples.forEach(function(sample) {
        samples.push(sample.value);
      });
      assert.deepEqual(samples, [0, 10, 0]);

      const samples1 = [];
      ctr.series[1].samples.forEach(function(sample) {
        samples1.push(sample.value);
      });
      assert.deepEqual(samples1, [7, 4, 1]);
      assert.deepEqual([0, 7,
        10, 14,
        0, 1], ctr.totals);
      assert.strictEqual(ctr.maxTotal, 14);
    }

    const events = [
      {name: 'ctr', args: {'value1': 0, 'value2': 7}, pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'C'}, // @suppress longLineCheck
      {name: 'ctr', args: {'value1': 10, 'value2': 4}, pid: 1, ts: 10, cat: 'foo', tid: 1, ph: 'C'}, // @suppress longLineCheck
      {name: 'ctr', args: {'value1': 0, 'value2': 1 }, pid: 1, ts: 20, cat: 'foo', tid: 1, ph: 'C'} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importObjectInsteadOfArray', function() {
    const events = { traceEvents: [
      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
    ] };

    const m = makeModel(events);
    assert.strictEqual(m.numProcesses, 1);
  });

  test('importString', function() {
    const events = [
      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
    ];

    const m = makeModel(JSON.stringify(events));
    assert.strictEqual(m.numProcesses, 1);
  });

  test('importStringWithLeadingSpaces', function() {
    const events = [
      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
    ];

    const m = makeModel(' ' + JSON.stringify(events));
    assert.strictEqual(m.numProcesses, 1);
  });

  test('importStringWithTrailingNewLine', function() {
    const events = [
      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
    ];

    const m = makeModel(JSON.stringify(events) + '\n');
    assert.strictEqual(m.numProcesses, 1);
  });

  test('importStringWithMissingCloseSquareBracket', function() {
    const events = [
      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
    ];

    const tmp = JSON.stringify(events);
    assert.strictEqual(tmp[tmp.length - 1], ']');

    // Drop off the trailing ]
    const dropped = tmp.substring(0, tmp.length - 1);
    const m = makeModel(dropped);
    assert.strictEqual(m.numProcesses, 1);
  });

  test('importStringWithEndingCommaButMissingCloseSquareBracket', function() {
    const lines = [
      '[',
      '{"name": "a", "args": {}, "pid": 52, "ts": 524, "cat": "foo", "tid": 53, "ph": "B"},', // @suppress longLineCheck
      '{"name": "a", "args": {}, "pid": 52, "ts": 560, "cat": "foo", "tid": 53, "ph": "E"},' // @suppress longLineCheck
    ];
    const text = lines.join('\n');

    const m = makeModel(text);
    assert.strictEqual(m.numProcesses, 1);
    assert.strictEqual(m.processes[52].threads[53].sliceGroup.length, 1);
  });

  test('importStringWithMissingCloseSquareBracketAndNewline', function() {
    const events = [
      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
    ];

    const tmp = JSON.stringify(events);
    assert.strictEqual(tmp[tmp.length - 1], ']');

    // Drop off the trailing ] and add a newline
    const dropped = tmp.substring(0, tmp.length - 1);
    const m = makeModel(dropped + '\n');
    assert.strictEqual(m.numProcesses, 1);
  });

  test('ImportStringEndingCommaButMissingCloseSquareBracketCRLF', function() {
    const lines = [
      '[',
      '{"name": "a", "args": {}, "pid": 52, "ts": 524, "cat": "foo", "tid": 53, "ph": "B"},', // @suppress longLineCheck
      '{"name": "a", "args": {}, "pid": 52, "ts": 560, "cat": "foo", "tid": 53, "ph": "E"},' // @suppress longLineCheck
    ];
    const text = lines.join('\r\n');

    const m = makeModel(text);
    assert.strictEqual(m.numProcesses, 1);
    assert.strictEqual(m.processes[52].threads[53].sliceGroup.length, 1);
  });

  test('importOldFormat', function() {
    const lines = [
      '[',
      '{"cat":"a","pid":9,"tid":8,"ts":194,"ph":"E","name":"I","args":{}},',
      '{"cat":"b","pid":9,"tid":8,"ts":194,"ph":"B","name":"I","args":{}}',
      ']'
    ];
    const text = lines.join('\n');
    const m = makeModel(text);
    assert.strictEqual(m.numProcesses, 1);
    assert.strictEqual(m.processes[9].threads[8].sliceGroup.length, 1);
  });

  test('startFinishOneSliceOneThread', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      assert.strictEqual(t.asyncSliceGroup.slices[0].title, 'a');
      assert.strictEqual(t.asyncSliceGroup.slices[0].category, 'cat');
      assert.isTrue(t.asyncSliceGroup.slices[0].isTopLevel);
      assert.strictEqual(t.asyncSliceGroup.slices[0].id, ':ptr:72');
      assert.strictEqual(t.asyncSliceGroup.slices[0].args.foo, 'bar');
      assert.strictEqual(t.asyncSliceGroup.slices[0].start, 0);
      assert.closeTo(
          (60 - 24) / 1000, t.asyncSliceGroup.slices[0].duration, 1e-5);
      assert.strictEqual(t.asyncSliceGroup.slices[0].startThread, t);
      assert.strictEqual(t.asyncSliceGroup.slices[0].endThread, t);
    }

    const events = [
      // Time is intentionally out of order.
      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'cat', tid: 53,
        ph: 'F', id: 72},
      {name: 'a', pid: 52, ts: 524, cat: 'cat', tid: 53,
        ph: 'S', id: 72, args: {'foo': 'bar'}}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('endArgsAddedToSlice', function() {
    function checkModel(m) {
      assert.strictEqual(m.numProcesses, 1);
      const p = m.processes[52];
      assert.isDefined(p);

      assert.strictEqual(p.numThreads, 1);
      const t = p.threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.sliceGroup.length, 1);
      assert.strictEqual(t.tid, 53);
      const slice = t.sliceGroup.slices[0];
      assert.strictEqual(slice.title, 'a');
      assert.strictEqual(slice.category, 'foo');
      assert.strictEqual(slice.start, 0);
      assert.strictEqual(slice.subSlices.length, 0);
      assert.strictEqual(slice.args.x, 1);
      assert.strictEqual(slice.args.y, 2);
    }

    const events = [
      {name: 'a', args: {x: 1}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
      {name: 'a', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('endArgOverrwritesOriginalArgValueIfDuplicated', function() {
    function checkModel(m) {
      assert.strictEqual(m.numProcesses, 1);
      const p = m.processes[52];
      assert.isDefined(p);

      assert.strictEqual(p.numThreads, 1);
      const t = p.threads[53];
      assert.isDefined(t);
      const slice = t.sliceGroup.slices[0];
      assert.strictEqual(slice.title, 'b');
      assert.strictEqual(slice.category, 'foo');
      assert.strictEqual(slice.start, 0);
      assert.strictEqual(slice.subSlices.length, 0);
      assert.strictEqual(slice.args.z, 4);
    }

    const events = [
      {name: 'b', args: {z: 3}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'B'},
      {name: 'b', args: {z: 4}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('asyncEndArgsAddedToSlice', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      const parentSlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(parentSlice.title, 'c');
      assert.strictEqual(parentSlice.category, 'foo');
      assert.isTrue(parentSlice.isTopLevel);
      assert.strictEqual(parentSlice.args.x, 1);
      assert.strictEqual(parentSlice.args.y, 2);

      assert.isDefined(parentSlice.subSlices);
      assert.strictEqual(parentSlice.subSlices.length, 0);
    }

    const events = [
      // Time is intentionally out of order.
      {name: 'c', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53,
        ph: 'F', id: 72},
      {name: 'c', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
        ph: 'S', id: 72}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('asyncEndArgOverwritesOriginalArgValueIfDuplicated', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      const parentSlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(parentSlice.title, 'd');
      assert.strictEqual(parentSlice.category, 'foo');
      assert.isTrue(parentSlice.isTopLevel);
      assert.strictEqual(parentSlice.args.z, 4);

      assert.isDefined(parentSlice.subSlices);
      assert.strictEqual(parentSlice.subSlices.length, 0);
    }

    const events = [
      // Time is intentionally out of order.
      {name: 'd', args: {z: 4}, pid: 52, ts: 560, cat: 'foo', tid: 53,
        ph: 'F', id: 72},
      {name: 'd', args: {z: 3}, pid: 52, ts: 524, cat: 'foo', tid: 53,
        ph: 'S', id: 72}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('asyncStepsInOneThread', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      const parentSlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(parentSlice.title, 'a');
      assert.strictEqual(parentSlice.category, 'foo');
      assert.isTrue(parentSlice.isTopLevel);
      assert.strictEqual(parentSlice.start, 0);
      assert.strictEqual(parentSlice.args.x, 1);
      assert.isUndefined(parentSlice.args.y);
      assert.strictEqual(parentSlice.args.z, 3);

      assert.isDefined(parentSlice.subSlices);
      assert.strictEqual(parentSlice.subSlices.length, 1);

      const subSlice = parentSlice.subSlices[0];
      assert.strictEqual(subSlice.title, 's1');
      assert.strictEqual(subSlice.category, 'foo');
      assert.isFalse(subSlice.isTopLevel);
      assert.closeTo((548 - 524) / 1000, subSlice.start, 1e-5);
      assert.closeTo((560 - 548) / 1000, subSlice.duration, 1e-5);
      assert.isUndefined(subSlice.args.x);
      assert.strictEqual(subSlice.args.y, 2);
      assert.isUndefined(subSlice.args.z);
    }

    const events = [
      // Time is intentionally out of order.
      {name: 'a', args: {z: 3}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'F', id: 72}, // @suppress longLineCheck
      {name: 'a', args: {step: 's1', y: 2}, pid: 52, ts: 548, cat: 'foo', tid: 53, ph: 'T', id: 72}, // @suppress longLineCheck
      {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'S', id: 72} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('asyncStepsMissingStart', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isUndefined(t);
    }

    const events = [
      // Time is intentionally out of order.
      {name: 'a', args: {z: 3}, pid: 52, ts: 560, cat: 'foo', tid: 53,
        ph: 'F', id: 72},
      {name: 'a', args: {step: 's1', y: 2}, pid: 52, ts: 548, cat: 'foo',
        tid: 53, ph: 'T', id: 72}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('autoClosingAsyncStepsMissingFinish', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      const parentSlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(parentSlice.title, 'a');
      assert.strictEqual(parentSlice.category, 'foo');
      assert.isTrue(parentSlice.isTopLevel);
      assert.strictEqual(parentSlice.start, 0);
      assert.strictEqual(parentSlice.duration, 2);
      assert.isTrue(parentSlice.didNotFinish);
      assert.isDefined(parentSlice.error);
      assert.isUndefined(parentSlice.args.y);
      assert.strictEqual(parentSlice.args.z, 3);

      assert.isDefined(parentSlice.subSlices);
      assert.strictEqual(parentSlice.subSlices.length, 1);
      const subSlice = parentSlice.subSlices[0];
      assert.strictEqual(subSlice.title, 's1');
      assert.strictEqual(subSlice.category, 'foo');
      assert.isFalse(subSlice.isTopLevel);
      assert.strictEqual(subSlice.start, 0);
      assert.strictEqual(subSlice.duration, 2);
      assert.isUndefined(subSlice.args.x);
      assert.strictEqual(subSlice.args.y, 2);
      assert.isUndefined(subSlice.args.z);
      assert.isTrue(subSlice.didNotFinish);
      assert.isDefined(subSlice.error);
    }

    const events = [
      {name: 'a', args: {z: 3}, pid: 52, ts: 0, cat: 'foo', tid: 53,
        ph: 'S', id: 72},
      {name: 'a', args: {step: 's1', y: 2}, pid: 52, ts: 0, cat: 'foo',
        tid: 53, ph: 'T', id: 72},

      // Slice that does finish to give an 'end time' to make autoclosing work.
      {name: 'b', args: {}, pid: 1, ts: 1000, cat: 'bar', tid: 2, ph: 'B'},
      {name: 'b', args: {}, pid: 1, ts: 2000, cat: 'bar', tid: 2, ph: 'E'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('asyncStepEndEvent', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      const parentSlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(parentSlice.title, 'a');
      assert.strictEqual(parentSlice.category, 'foo');
      assert.isTrue(parentSlice.isTopLevel);
      assert.strictEqual(parentSlice.start, 0);
      assert.strictEqual(parentSlice.args.x, 1);
      assert.isUndefined(parentSlice.args.y);
      assert.strictEqual(parentSlice.args.z, 3);

      assert.isDefined(parentSlice.subSlices);
      assert.strictEqual(parentSlice.subSlices.length, 1);
      const subSlice = parentSlice.subSlices[0];
      assert.strictEqual(subSlice.title, 's1');
      assert.strictEqual(subSlice.category, 'foo');
      assert.isFalse(subSlice.isTopLevel);
      assert.strictEqual(subSlice.start, 0);
      assert.closeTo((548 - 524) / 1000, subSlice.duration, 1e-5);
      assert.isUndefined(subSlice.args.x);
      assert.strictEqual(subSlice.args.y, 2);
      assert.isUndefined(subSlice.args.z);
    }

    const events = [
      // Time is intentionally out of order.
      {name: 'a', args: {z: 3}, pid: 52, ts: 560, cat: 'foo', tid: 53,
        ph: 'F', id: 72},
      {name: 'a', args: {step: 's1', y: 2}, pid: 52, ts: 548, cat: 'foo',
        tid: 53, ph: 'p', id: 72},
      {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
        ph: 'S', id: 72}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('asyncStepMismatch', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isUndefined(t);
      assert.isTrue(m.hasImportWarnings);
    }

    const events = [
      // Time is intentionally out of order.
      {name: 'a', args: {z: 3}, pid: 52, ts: 560, cat: 'foo', tid: 53,
        ph: 'F', id: 72},
      {name: 'a', args: {step: 's2'}, pid: 52, ts: 548, cat: 'foo', tid: 53,
        ph: 'T', id: 72},
      {name: 'a', args: {step: 's1'}, pid: 52, ts: 548, cat: 'foo', tid: 53,
        ph: 'p', id: 72},
      {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
        ph: 'S', id: 72}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('asyncSliceWithoutCPUDuration', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 3);

      const noTTSNoField = t.asyncSliceGroup.slices[0];
      assert.isUndefined(noTTSNoField.cpuStart);
      assert.isUndefined(noTTSNoField.cpuDuration);

      const TTSNoField = t.asyncSliceGroup.slices[1];
      assert.isUndefined(TTSNoField.cpuStart);
      assert.isUndefined(TTSNoField.cpuDuration);

      const TTSZeroField = t.asyncSliceGroup.slices[2];
      assert.isUndefined(TTSZeroField.cpuStart);
      assert.isUndefined(TTSZeroField.cpuDuration);
    }

    const events = [
      // Async slice without tts field.
      {name: 'a', args: {params: ''}, pid: 52, ts: 10, cat: 'foo', tid: 53,
        id: 72, ph: 'b'},
      {name: 'a', args: {params: ''}, pid: 52, ts: 20, cat: 'foo', tid: 53,
        id: 72, ph: 'e'},
      // Async slice with tts field but without use_async_tts marker field.
      {name: 'b', args: {params: ''}, pid: 52, ts: 30, cat: 'foo', tid: 53,
        id: 72, ph: 'b', tts: 30000},
      {name: 'b', args: {params: ''}, pid: 52, ts: 40, cat: 'foo', tid: 53,
        id: 72, ph: 'e', tts: 40000},
      // Async slice with tts field but with use_async_tts marker set to 0.
      {name: 'c', args: {params: ''}, pid: 52, ts: 50000, cat: 'foo', tid: 53,
        id: 72, ph: 'b', tts: 50000, use_async_tts: 0},
      {name: 'c', args: {params: ''}, pid: 52, ts: 60000, cat: 'foo', tid: 53,
        id: 72, ph: 'e', tts: 60000, use_async_tts: 0}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('asyncSliceWithCPUDuration', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);

      const asyncSlice = t.asyncSliceGroup.slices[0];
      assert.isDefined(asyncSlice);
      assert.strictEqual(asyncSlice.duration, 10);
      assert.strictEqual(asyncSlice.cpuStart, 100);
      assert.strictEqual(asyncSlice.cpuDuration, 5);
    }

    const events = [
      {name: 'a', args: {params: ''}, pid: 52, ts: 50000, cat: 'foo', tid: 53,
        id: 72, ph: 'b', tts: 100000, use_async_tts: 1},
      {name: 'a', args: {params: ''}, pid: 52, ts: 60000, cat: 'foo', tid: 53,
        id: 72, ph: 'e', tts: 105000, use_async_tts: 1}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('nestableAsyncBasic', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      const parentSlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(parentSlice.title, 'a');
      assert.strictEqual(parentSlice.category, 'foo');
      assert.isTrue(parentSlice.isTopLevel);

      assert.isDefined(parentSlice.subSlices);
      assert.strictEqual(parentSlice.subSlices.length, 1);
      const subSlice = parentSlice.subSlices[0];
      assert.isFalse(subSlice.isTopLevel);
      // Arguments should include both BEGIN and END event.
      assert.strictEqual(subSlice.args.x, 1);
      assert.strictEqual(subSlice.args.y, 2);
      assert.sameMembers(subSlice.subSlices, []);
    }

    const events = [
      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'b', args: {x: 1}, pid: 52, ts: 525, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'b', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53,
        ph: 'e', id: 72},
      {name: 'a', args: {}, pid: 52, ts: 565, cat: 'foo', tid: 53,
        ph: 'e', id: 72}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });


  test('nestableAsyncNoArgs', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      const slice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(slice.title, 'name');
      assert.strictEqual(slice.category, 'foo');
      assert.isTrue(slice.isTopLevel);

      assert.isDefined(slice.subSlices);
      assert.strictEqual(slice.subSlices.length, 0);

      assert.deepEqual(slice.args, {});
    }

    const events = [
      {name: 'name', pid: 52, ts: 525, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'name', pid: 52, ts: 560, cat: 'foo', tid: 53,
        ph: 'e', id: 72}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('nestableAsyncCombinedParams', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 3);

      const sliceA = t.asyncSliceGroup.slices[0];
      // Arguments should include both BEGIN and END event.
      assert.strictEqual(sliceA.args.x, 1);
      assert.strictEqual(sliceA.args.y, 2);
      const paramsA = sliceA.args.params;
      assert.isDefined(paramsA);
      assert.strictEqual(paramsA.p1, 'hello');
      assert.strictEqual(paramsA.p2, 123);
      assert.strictEqual(paramsA.p3, 'hi');
      assert.isTrue(sliceA.isTopLevel);

      const sliceB = t.asyncSliceGroup.slices[1];
      // Arguments should include both BEGIN and END event.
      const paramsB = sliceB.args.params;
      assert.isDefined(paramsB);
      assert.strictEqual(paramsB.p4, 'foo');
      assert.isTrue(sliceB.isTopLevel);

      const sliceC = t.asyncSliceGroup.slices[2];
      // Arguments should include both BEGIN and END event.
      const paramsC = sliceC.args.params;
      assert.isDefined(paramsC);
      assert.strictEqual(paramsC.p5, 'bar');
      assert.isTrue(sliceC.isTopLevel);
    }

    const events = [
      {name: 'a', args: {x: 1, params: {p1: 'hello', p2: 123}},
        pid: 52, ts: 525, cat: 'foo', tid: 53, ph: 'b', id: 72},
      {name: 'a', args: {y: 2, params: {p3: 'hi'}}, pid: 52, ts: 560,
        cat: 'foo', tid: 53, ph: 'e', id: 72},
      {name: 'b', args: {params: {p4: 'foo'}},
        pid: 52, ts: 525, cat: 'foo', tid: 53, ph: 'b', id: 73},
      {name: 'b', args: {params: ''}, pid: 52, ts: 560,
        cat: 'foo', tid: 53, ph: 'e', id: 73},
      {name: 'c', args: {params: {p5: 'bar'}},
        pid: 52, ts: 525, cat: 'foo', tid: 53, ph: 'b', id: 74},
      {name: 'c', args: {}, pid: 52, ts: 560,
        cat: 'foo', tid: 53, ph: 'e', id: 74}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('nestableAsyncManyLevels', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      // Perfectly matched events should not produce a warning.
      assert.isFalse(m.hasImportWarnings);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);

      const l1Slice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(l1Slice.title, 'l1');
      assert.closeTo(0, l1Slice.start, 1e-5);
      assert.closeTo(9 / 1000, l1Slice.duration, 1e-5);
      assert.isTrue(l1Slice.isTopLevel);

      assert.isDefined(l1Slice.subSlices);
      assert.strictEqual(l1Slice.subSlices.length, 1);
      const l2Slice = l1Slice.subSlices[0];
      assert.strictEqual(l2Slice.title, 'l2');
      assert.closeTo(1 / 1000, l2Slice.start, 1e-5);
      assert.closeTo(7 / 1000, l2Slice.duration, 1e-5);
      assert.isFalse(l2Slice.isTopLevel);

      assert.isDefined(l2Slice.subSlices);
      assert.strictEqual(l2Slice.subSlices.length, 1);
      const l3Slice = l2Slice.subSlices[0];
      assert.strictEqual(l3Slice.title, 'l3');
      assert.closeTo(2 / 1000, l3Slice.start, 1e-5);
      assert.closeTo(5 / 1000, l3Slice.duration, 1e-5);
      assert.isFalse(l3Slice.isTopLevel);

      assert.isDefined(l3Slice.subSlices);
      assert.strictEqual(l3Slice.subSlices.length, 1);
      const l4Slice = l3Slice.subSlices[0];
      assert.strictEqual(l4Slice.title, 'l4');
      assert.closeTo(3 / 1000, l4Slice.start, 1e-5);
      assert.closeTo(3 / 1000, l4Slice.duration, 1e-5);
      assert.isFalse(l4Slice.isTopLevel);

      assert.isDefined(l4Slice.subSlices);
      assert.strictEqual(l4Slice.subSlices.length, 1);
      const l5Slice = l4Slice.subSlices[0];
      assert.strictEqual(l5Slice.title, 'l5');
      assert.closeTo(4 / 1000, l5Slice.start, 1e-5);
      assert.closeTo(1 / 1000, l5Slice.duration, 1e-5);
      assert.isFalse(l5Slice.isTopLevel);
    }

    // There are 5 nested levels.
    const events = [
      {name: 'l1', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'l2', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'l3', args: {}, pid: 52, ts: 526, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'l4', args: {}, pid: 52, ts: 527, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'l5', args: {}, pid: 52, ts: 528, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'l5', args: {}, pid: 52, ts: 529, cat: 'foo', tid: 53,
        ph: 'e', id: 72},
      {name: 'l4', args: {}, pid: 52, ts: 530, cat: 'foo', tid: 53,
        ph: 'e', id: 72},
      {name: 'l3', args: {}, pid: 52, ts: 531, cat: 'foo', tid: 53,
        ph: 'e', id: 72},
      {name: 'l2', args: {}, pid: 52, ts: 532, cat: 'foo', tid: 53,
        ph: 'e', id: 72},
      {name: 'l1', args: {}, pid: 52, ts: 533, cat: 'foo', tid: 53,
        ph: 'e', id: 72}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('nestableAsyncInstantEvent', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 2);
      const instantSlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(instantSlice.title, 'c');
      assert.closeTo(0, instantSlice.start, 1e-5);
      assert.closeTo(0, instantSlice.duration, 1e-5);
      assert.sameMembers(instantSlice.subSlices, []);
      assert.isTrue(instantSlice.isTopLevel);

      const nestedSlice = t.asyncSliceGroup.slices[1];
      assert.strictEqual(nestedSlice.title, 'a');
      assert.closeTo(0, nestedSlice.start, 1e-5);
      assert.closeTo((565 - 524) / 1000, nestedSlice.duration, 1e-5);
      assert.isTrue(nestedSlice.isTopLevel);
      assert.isDefined(nestedSlice.subSlices);
      assert.strictEqual(nestedSlice.subSlices.length, 1);
      const nestedInstantSlice = nestedSlice.subSlices[0];
      assert.sameMembers(nestedInstantSlice.subSlices, []);
      assert.strictEqual(nestedInstantSlice.title, 'd');
      assert.isFalse(nestedInstantSlice.isTopLevel);
    }

    const events = [
      {name: 'c', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
        ph: 'n', id: 71},
      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'd', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
        ph: 'n', id: 72},
      {name: 'a', args: {}, pid: 52, ts: 565, cat: 'foo', tid: 53,
        ph: 'e', id: 72}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('nestableAsyncUnmatchedOuterBeginEvent', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      // Unmatched BEGIN should produce a warning.
      assert.isTrue(m.hasImportWarnings);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      const parentSlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(parentSlice.title, 'a');
      assert.strictEqual(parentSlice.category, 'foo');
      assert.isTrue(parentSlice.isTopLevel);
      assert.closeTo(0, parentSlice.start, 0.0001);
      // Unmatched BEGIN event ends at the last event of that ID.
      assert.closeTo(36 / 1000, parentSlice.duration, 0.0001);
      // Arguments should include only include its arguments.
      assert.isUndefined(parentSlice.args.y);
      assert.strictEqual(parentSlice.args.x, 1);
      assert.isDefined(parentSlice.error);

      assert.isDefined(parentSlice.subSlices);
      assert.strictEqual(parentSlice.subSlices.length, 1);
      const subSlice = parentSlice.subSlices[0];
      assert.isFalse(subSlice.isTopLevel);
      assert.closeTo(1 / 1000, subSlice.start, 1e-5);
      assert.closeTo(35 / 1000, subSlice.duration, 1e-5);
      assert.sameMembers(subSlice.subSlices, []);
      // Arguments should include those of the END event.
      assert.strictEqual(subSlice.args.y, 2);
      assert.sameMembers(subSlice.subSlices, []);
    }

    const events = [
      {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'b', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'b', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53,
        ph: 'e', id: 72}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('nestableAsyncUnmatchedInnerBeginEvent', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      // Unmatched BEGIN should produce a warning.
      assert.isTrue(m.hasImportWarnings);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      const parentSlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(parentSlice.title, 'a');
      assert.strictEqual(parentSlice.category, 'foo');
      assert.isTrue(parentSlice.isTopLevel);
      assert.closeTo(0, parentSlice.start, 1e-5);
      assert.closeTo(41 / 1000, parentSlice.duration, 1e-5);
      // Arguments should include both BEGIN and END event.
      assert.strictEqual(parentSlice.args.y, 2);
      assert.strictEqual(parentSlice.args.z, 3);
      assert.isUndefined(parentSlice.args.x);

      assert.isDefined(parentSlice.subSlices);
      assert.strictEqual(parentSlice.subSlices.length, 2);
      const subSliceInstant = parentSlice.subSlices[0];
      const subSliceUnmatched = parentSlice.subSlices[1];
      assert.strictEqual(subSliceInstant.title, 'c');
      assert.isFalse(subSliceInstant.isTopLevel);
      assert.strictEqual(subSliceUnmatched.title, 'b');
      assert.isFalse(subSliceUnmatched.isTopLevel);
      // Unmatched BEGIN ends at the last event of that ID.
      assert.closeTo(1 / 1000, subSliceUnmatched.start, 1e-5);
      assert.closeTo(40 / 1000, subSliceUnmatched.duration, 1e-5);
      assert.sameMembers(subSliceUnmatched.subSlices, []);
      assert.strictEqual(subSliceUnmatched.args.x, 1);
      assert.isUndefined(subSliceUnmatched.y);
      assert.isDefined(subSliceUnmatched.error);
      assert.closeTo(1 / 1000, subSliceInstant.start, 1e-5);
      assert.closeTo(0, subSliceInstant.duration, 1e-5);
      assert.sameMembers(subSliceInstant.subSlices, []);
    }

    const events = [
      {name: 'a', args: {z: 3}, pid: 52, ts: 524, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'c', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
        ph: 'n', id: 72},
      {name: 'b', args: {x: 1}, pid: 52, ts: 525, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'a', args: {y: 2}, pid: 52, ts: 565, cat: 'foo', tid: 53,
        ph: 'e', id: 72}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('nestableAsyncUnmatchedOuterEndEvent', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      // Unmatched END should produce a warning.
      assert.isTrue(m.hasImportWarnings);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 2);
      const unmatchedSlice = t.asyncSliceGroup.slices[0];
      const slice = t.asyncSliceGroup.slices[1];
      assert.strictEqual(unmatchedSlice.title, 'a');
      assert.closeTo(0, unmatchedSlice.start, 1e-5);
      assert.isTrue(unmatchedSlice.isTopLevel);
      // Unmatched END event begins at the first event of that ID. In this
      // case, the first event happens to be the same unmatched event.
      assert.closeTo(0 / 1000, unmatchedSlice.duration, 1e-5);
      assert.isUndefined(unmatchedSlice.args.x);
      assert.isUndefined(unmatchedSlice.args.y);
      assert.strictEqual(unmatchedSlice.args.z, 3);
      assert.isDefined(unmatchedSlice.error);
      assert.sameMembers(unmatchedSlice.subSlices, []);

      assert.strictEqual(slice.title, 'b');
      assert.isTrue(slice.isTopLevel);
      assert.closeTo(1 / 1000, slice.start, 1e-5);
      assert.closeTo(35 / 1000, slice.duration, 1e-5);
      // Arguments should include both BEGIN and END event.
      assert.strictEqual(slice.args.x, 1);
      assert.strictEqual(slice.args.y, 2);
      assert.sameMembers(slice.subSlices, []);
    }

    // Events are intentionally out-of-order.
    const events = [
      {name: 'b', args: {x: 1}, pid: 52, ts: 525, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'b', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53,
        ph: 'e', id: 72},
      {name: 'a', args: {z: 3}, pid: 52, ts: 524, cat: 'foo', tid: 53,
        ph: 'e', id: 72}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('nestableAsyncUnmatchedInnerEndEvent', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      // Unmatched END should produce a warning.
      assert.isTrue(m.hasImportWarnings);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      const parentSlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(parentSlice.title, 'a');
      assert.isTrue(parentSlice.isTopLevel);
      assert.closeTo(0, parentSlice.start, 1e-5);
      assert.closeTo(41 / 1000, parentSlice.duration, 1e-5);
      // Arguments should include both BEGIN and END event.
      assert.strictEqual(parentSlice.args.x, 1);
      assert.strictEqual(parentSlice.args.y, 2);

      assert.isDefined(parentSlice.subSlices);
      assert.strictEqual(parentSlice.subSlices.length, 2);
      const subSliceInstant = parentSlice.subSlices[0];
      const subSliceUnmatched = parentSlice.subSlices[1];
      assert.strictEqual(subSliceInstant.title, 'c');
      assert.isFalse(subSliceInstant.isTopLevel);
      assert.strictEqual(subSliceUnmatched.title, 'b');
      assert.isFalse(subSliceUnmatched.isTopLevel);
      // Unmatched END begins at the first event of that ID.
      assert.closeTo(0 / 1000, subSliceUnmatched.start, 1e-5);
      assert.closeTo(1 / 1000, subSliceUnmatched.duration, 1e-5);
      // Arguments should include both BEGIN and END event.
      assert.isUndefined(subSliceUnmatched.args.x);
      assert.isUndefined(subSliceUnmatched.args.y);
      assert.strictEqual(subSliceUnmatched.args.z, 3);
      assert.isDefined(subSliceUnmatched.error);

      assert.sameMembers(subSliceUnmatched.subSlices, []);
      assert.closeTo(1 / 1000, subSliceInstant.start, 1e-5);
      assert.closeTo(0, subSliceInstant.duration, 1e-5);
      assert.sameMembers(subSliceInstant.subSlices, []);
    }

    const events = [
      {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'c', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
        ph: 'n', id: 72},
      {name: 'b', args: {z: 3}, pid: 52, ts: 525, cat: 'foo', tid: 53,
        ph: 'e', id: 72},
      {name: 'a', args: {y: 2}, pid: 52, ts: 565, cat: 'foo', tid: 53,
        ph: 'e', id: 72}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('nestableAsyncSameIDDifferentCategory', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 2);
      const eventASlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(eventASlice.title, 'EVENT_A');
      assert.strictEqual(eventASlice.category, 'foo');
      assert.strictEqual(eventASlice.id, 'foo::ptr:72');
      assert.isTrue(eventASlice.isTopLevel);
      assert.strictEqual(eventASlice.args.x, 1);
      assert.sameMembers(eventASlice.subSlices, []);

      const eventBSlice = t.asyncSliceGroup.slices[1];
      assert.strictEqual(eventBSlice.title, 'EVENT_B');
      assert.strictEqual(eventBSlice.category, 'bar');
      assert.strictEqual(eventBSlice.id, 'bar::ptr:72');
      assert.isTrue(eventBSlice.isTopLevel);
      assert.strictEqual(eventBSlice.args.y, 2);
      assert.sameMembers(eventBSlice.subSlices, []);
    }

    // Events with the same ID, but different categories should not be
    // considered as nested.
    const events = [
      {name: 'EVENT_A', args: {}, pid: 52, ts: 500, cat: 'foo', tid: 53,
        ph: 'b', id: 72},
      {name: 'EVENT_B', args: {y: 2}, pid: 52, ts: 550, cat: 'bar', tid: 53,
        ph: 'b', id: 72},
      {name: 'EVENT_B', args: {}, pid: 52, ts: 600, cat: 'bar', tid: 53,
        ph: 'e', id: 72},
      {name: 'EVENT_A', args: {x: 1}, pid: 52, ts: 650, cat: 'foo', tid: 53,
        ph: 'e', id: 72}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('nestableAsyncStackFrame', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      const slice = t.asyncSliceGroup.slices[0];

      assert.strictEqual(slice.startStackFrame.title, 'main');
      assert.strictEqual(slice.endStackFrame.title, 'frame7');
    }

    const events = {
      traceEvents: [
        {name: 'name', pid: 52, ts: 525, cat: 'foo', tid: 53,
          ph: 'b', id: 72, sf: 1},
        {name: 'name', pid: 52, ts: 560, cat: 'foo', tid: 53,
          ph: 'e', id: 72, sf: 7}
      ],
      stackFrames: {
        '1': {
          category: 'm1',
          name: 'main'
        },
        '7': {
          category: 'm2',
          name: 'frame7',
          parent: '1'
        }
      }
    };
    checkParsedAndStreamInput(events, checkModel);
  });

  test('processLocalAsync', function() {
    function checkModel(m) {
      let t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      let parentSlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(parentSlice.title, 'a');
      assert.strictEqual(parentSlice.category, 'foo');
      assert.isTrue(parentSlice.isTopLevel);
      assert.isDefined(parentSlice.subSlices);
      assert.strictEqual(parentSlice.subSlices.length, 0);

      t = m.processes[54].threads[55];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      parentSlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(parentSlice.title, 'b');
      assert.strictEqual(parentSlice.category, 'foo');
      assert.strictEqual(parentSlice.args.x, 1);
      assert.strictEqual(parentSlice.args.y, 2);
      assert.isTrue(parentSlice.isTopLevel);
      assert.isDefined(parentSlice.subSlices);
      assert.strictEqual(parentSlice.subSlices.length, 0);
    }

    const events = [
      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
        ph: 'b', id2: {local: 72}},
      {name: 'b', args: {x: 1}, pid: 54, ts: 525, cat: 'foo', tid: 55,
        ph: 'b', id2: {local: 72}},
      {name: 'b', args: {y: 2}, pid: 54, ts: 560, cat: 'foo', tid: 55,
        ph: 'e', id2: {local: 72}},
      {name: 'a', args: {}, pid: 52, ts: 565, cat: 'foo', tid: 53,
        ph: 'e', id2: {local: 72}}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('globalAsync', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      const parentSlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(parentSlice.title, 'a');
      assert.strictEqual(parentSlice.category, 'foo');
      assert.isTrue(parentSlice.isTopLevel);

      assert.isDefined(parentSlice.subSlices);
      assert.strictEqual(parentSlice.subSlices.length, 1);
      const subSlice = parentSlice.subSlices[0];
      assert.isFalse(subSlice.isTopLevel);
      // Arguments should include both BEGIN and END event.
      assert.strictEqual(subSlice.args.x, 1);
      assert.strictEqual(subSlice.args.y, 2);
      assert.sameMembers(subSlice.subSlices, []);

      assert.isUndefined(m.processes[54].threads[55]);
    }

    const events = [
      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
        ph: 'b', id2: {global: 72}},
      {name: 'b', args: {x: 1}, pid: 54, ts: 525, cat: 'foo', tid: 55,
        ph: 'b', id2: {global: 72}},
      {name: 'b', args: {y: 2}, pid: 54, ts: 560, cat: 'foo', tid: 55,
        ph: 'e', id2: {global: 72}},
      {name: 'a', args: {}, pid: 52, ts: 565, cat: 'foo', tid: 53,
        ph: 'e', id2: {global: 72}}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importSamples', function() {
    function checkModel(m) {
      const p = m.processes[52];
      assert.isDefined(p);
      const t = p.threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.samples_.length, 4);
      assert.strictEqual(t.samples_[0].start, 0.0);
      assert.strictEqual(t.samples_[1].start, 0.0);
      assert.closeTo(0.01, t.samples_[2].start, 1e-5);
      assert.strictEqual(t.samples_[0].leafNode.title, 'a');
      assert.strictEqual(t.samples_[1].leafNode.title, 'b');
      assert.strictEqual(t.samples_[2].leafNode.title, 'c');
      assert.strictEqual(t.samples_[3].leafNode, t.samples[0].leafNode);
      assert.isFalse(m.hasImportWarnings);
    }

    const events = [
      {name: 'a', args: {}, pid: 52, ts: 548, cat: 'test', tid: 53, ph: 'P'},
      {name: 'b', args: {}, pid: 52, ts: 548, cat: 'test', tid: 53, ph: 'P'},
      {name: 'c', args: {}, pid: 52, ts: 558, cat: 'test', tid: 53, ph: 'P'},
      {name: 'a', args: {}, pid: 52, ts: 568, cat: 'test', tid: 53, ph: 'P'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importSamplesWithStackFrames', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[2];

      assert.strictEqual(t.samples.length, 1);
      assert.strictEqual(t.samples_[0].start, 0.0);
      assert.strictEqual(t.samples_[0].leafNode.title, 'frame7');
      assert.isFalse(m.hasImportWarnings);
    }

    const eventData = {
      traceEvents: [
        { name: 'a', args: {}, pid: 1, ts: 0, cat: 'test', tid: 2, ph: 'P', sf: 7 } // @suppress longLineCheck
      ],
      stackFrames: {
        '1': {
          category: 'm1',
          name: 'main'
        },
        '7': {
          category: 'm2',
          name: 'frame7',
          parent: '1'
        }
      }
    };
    checkParsedAndStreamInput(eventData, checkModel);
  });

  test('importSamplesMissingArgs', function() {
    function checkModel(m) {
      const p = m.processes[52];
      assert.isDefined(p);
      const t = p.threads[53];
      assert.isDefined(t);
      assert.isDefined(t);
      assert.strictEqual(t.samples_.length, 3);
      assert.isFalse(m.hasImportWarnings);
    }

    const events = [
      {name: 'a', pid: 52, ts: 548, cat: 'test', tid: 53, ph: 'P'},
      {name: 'b', pid: 52, ts: 548, cat: 'test', tid: 53, ph: 'P'},
      {name: 'c', pid: 52, ts: 549, cat: 'test', tid: 53, ph: 'P'}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importV8Samples', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[2];

      assert.isFalse(m.hasImportWarnings);
      assert.strictEqual(t.samples.length, 2);

      let sample = t.samples_[0];
      assert.deepEqual(
          sample.userFriendlyStack,
          ['foo url: http://example.com/bar.js:22', 'bar url: unknown']);

      sample = t.samples_[1];
      assert.deepEqual(sample.userFriendlyStack, ['gc url: unknown']);
    }

    const eventData = {
      traceEvents: [
        { name: 'V8Sample', args: {data: {stack: ['0x2a574306061', '0x2a574306224'], vm_state: 'js'}}, pid: 1, ts: 4, cat: 'test', tid: 2, ph: 'P' }, // @suppress longLineCheck
        { name: 'V8Sample', args: {data: {stack: [], vm_state: 'gc'}}, pid: 1, ts: 6, cat: 'test', tid: 2, ph: 'P' }, // @suppress longLineCheck
        { name: 'JitCodeAdded', args: {data: {code_len: 2, name: 'LazyCompile:~foo http://example.com/bar.js:23', code_start: '0x2a574306060'}}, pid: 1, ts: 1, cat: 'test', tid: 2, ph: 'M' }, // @suppress longLineCheck
        { name: 'JitCodeAdded', args: {data: {code_len: 20, name: 'bar', code_start: '0x2a574306220'}}, pid: 1, ts: 2, cat: 'test', tid: 2, ph: 'M' }, // @suppress longLineCheck
        { name: 'JitCodeMoved', args: {data: {code_len: 2, old_code_start: '0x2a574306220', code_start: '0x2a574306222'}}, pid: 1, ts: 3, cat: 'test', tid: 2, ph: 'M' }, // @suppress longLineCheck
        { name: 'JitCodeAdded', args: {data: {code_len: 20, name: 'baz', code_start: '0xffffffff9f90a1a0'}}, pid: 1, ts: 4, cat: 'test', tid: 2, ph: 'M' } // @suppress longLineCheck
      ]
    };
    checkParsedAndStreamInput(eventData, checkModel);
  });

  test('importOldFormatV8Samples', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[2];

      assert.isFalse(m.hasImportWarnings);
      assert.strictEqual(t.samples.length, 2);

      let sample = t.samples_[0];
      assert.deepEqual(
          sample.userFriendlyStack,
          ['foo url: http://example.com/bar.js:22', 'bar url: unknown']);

      sample = t.samples_[1];
      assert.deepEqual(sample.userFriendlyStack, ['gc url: unknown']);
    }

    const eventData = {
      traceEvents: [
        { name: 'JitCodeAdded', args: {data: {code_len: 2, name: 'LazyCompile:~foo http://example.com/bar.js:23', code_start: '0x2a574306060'}}, pid: 1, ts: 0, cat: 'test', tid: 2, ph: 'I' }, // @suppress longLineCheck
        { name: 'JitCodeAdded', args: {data: {code_len: 20, name: 'bar', code_start: '0x2a574306220'}}, pid: 1, ts: 0, cat: 'test', tid: 2, ph: 'I' }, // @suppress longLineCheck
        { name: 'JitCodeMoved', args: {data: {code_len: 2, old_code_start: '0x2a574306220', code_start: '0x2a574306222'}}, pid: 1, ts: 0, cat: 'test', tid: 2, ph: 'I' }, // @suppress longLineCheck
        { name: 'JitCodeAdded', args: {data: {code_len: 20, name: 'baz', code_start: '0xffffffff9f90a1a0'}}, pid: 1, ts: 0, cat: 'test', tid: 2, ph: 'I' }, // @suppress longLineCheck
        { name: 'V8Sample', args: {data: {stack: ['0x2a574306061', '0x2a574306224']}}, pid: 1, ts: 0, cat: 'test', tid: 2, ph: 'P' }, // @suppress longLineCheck
        { name: 'V8Sample', args: {data: {stack: [], vm_state: 'gc'}}, pid: 1, ts: 10, cat: 'test', tid: 2, ph: 'P' } // @suppress longLineCheck
      ]
    };
    checkParsedAndStreamInput(eventData, checkModel);
  });

  test('importSimpleObject', function() {
    function checkModel(m) {
      assert.strictEqual(m.bounds.min, 10);
      assert.strictEqual(m.bounds.max, 50);
      assert.isFalse(m.hasImportWarnings);

      const p = m.processes[1];
      assert.isDefined(p);

      const i10 = p.objects.getObjectInstanceAt(
          new ScopedId('ptr', '0x1000'), 10);
      assert.strictEqual(i10.category, 'c');
      assert.strictEqual(i10.creationTs, 10);
      assert.strictEqual(i10.deletionTs, 50);
      assert.strictEqual(i10.snapshots.length, 2);

      const s15 = i10.snapshots[0];
      assert.strictEqual(s15.ts, 15);
      assert.strictEqual(s15.args, 15);

      const s20 = i10.snapshots[1];
      assert.strictEqual(s20.ts, 20);
      assert.strictEqual(s20.args, 20);
    }

    const events = [
      {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
      {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: {snapshot: 15}}, // @suppress longLineCheck
      {ts: 20000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: {snapshot: 20}}, // @suppress longLineCheck
      {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'a', args: {}} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('importImplicitObjects', function() {
    function checkModel(m) {
      const p1 = m.processes[1];

      const iA = p1.objects.getObjectInstanceAt(
          new ScopedId('ptr', '0x1000'), 10);
      const subObjectInstances =
          p1.objects.getAllInstancesByTypeName().subObject;

      assert.strictEqual(subObjectInstances.length, 2);
      const subObject1 = p1.objects.getObjectInstanceAt(
          new ScopedId('ptr', '0x1'), 15);
      assert.strictEqual(subObject1.name, 'subObject');
      assert.strictEqual(subObject1.creationTs, 15);

      assert.strictEqual(subObject1.snapshots.length, 2);
      assert.strictEqual(subObject1.snapshots[0].ts, 15);
      assert.strictEqual(subObject1.snapshots[0].args.foo, 1);
      assert.strictEqual(subObject1.snapshots[1].ts, 20);
      assert.strictEqual(subObject1.snapshots[1].args.foo, 2);

      const subObject2 = p1.objects.getObjectInstanceAt(
          new ScopedId('ptr', '0x2'), 20);
      assert.strictEqual(subObject2.name, 'subObject');
      assert.strictEqual(subObject2.creationTs, 20);
      assert.strictEqual(subObject2.snapshots.length, 1);
      assert.strictEqual(subObject2.snapshots[0].ts, 20);
    }

    const events = [
      {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
      {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a',
        args: { snapshot: [
          { id: 'subObject/0x1',
            foo: 1
          }
        ]}},
      {ts: 20000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a',
        args: { snapshot: [
          { id: 'subObject/0x1',
            foo: 2
          },
          { id: 'subObject/0x2',
            foo: 1
          }
        ]}}
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('importImplicitObjectWithCategoryOverride', function() {
    function checkModel(m) {
      const p1 = m.processes[1];

      const iA = p1.objects.getObjectInstanceAt(
          new ScopedId('ptr', '0x1000'), 10);
      const subObjectInstances =
          p1.objects.getAllInstancesByTypeName().subObject;

      assert.strictEqual(subObjectInstances.length, 1);
    }

    const events = [
      {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'cat', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
      {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'otherCat', id: '0x1000', name: 'a', // @suppress longLineCheck
        args: { snapshot: [
          { id: 'subObject/0x1',
            cat: 'cat',
            foo: 1
          }
        ]}}
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importImplicitObjectWithBaseTypeOverride', function() {
    function checkModel(m) {
      const p1 = m.processes[1];
      assert.strictEqual(m.importWarnings.length, 0);

      const iA = p1.objects.getObjectInstanceAt(
          new ScopedId('ptr', '0x1000'), 10);
      assert.strictEqual(iA.snapshots.length, 1);
    }

    const events = [
      {ts: 10000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'PictureLayerImpl', args: { // @suppress longLineCheck
        snapshot: {
          base_type: 'LayerImpl'
        }
      }},
      {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'LayerImpl', args: {}} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importIDRefs', function() {
    function checkModel(m) {
      const p1 = m.processes[1];

      const iA = p1.objects.getObjectInstanceAt(
          new ScopedId('ptr', '0x1000'), 10);
      const s15 = iA.getSnapshotAt(15);

      const taskSlice = p1.threads[1].sliceGroup.slices[0];
      assert.strictEqual(taskSlice.args.my_object, s15);
    }

    const events = [
      // An object with two snapshots.
      {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
      {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: {snapshot: 15}}, // @suppress longLineCheck
      {ts: 20000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: {snapshot: 20}}, // @suppress longLineCheck
      {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck

      // A slice that references the object.
      {ts: 17000, pid: 1, tid: 1, ph: 'B', cat: 'c', name: 'taskSlice', args: {my_object: {id_ref: '0x1000'}}}, // @suppress longLineCheck
      {ts: 17500, pid: 1, tid: 1, ph: 'E', cat: 'c', name: 'taskSlice', args: {}} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('importIDRefsThatPointAtEachOther', function() {
    function checkModel(m) {
      const p1 = m.processes[1];

      const iA = p1.objects.getObjectInstanceAt(
          new ScopedId('ptr', '0x1000'), 15);
      const iFoo = p1.objects.getObjectInstanceAt(
          new ScopedId('ptr', '0x1001'), 15);
      assert.isDefined(iA);
      assert.isDefined(iFoo);

      const a15 = iA.getSnapshotAt(15);
      const foo15 = iFoo.getSnapshotAt(15);

      const taskSlice = p1.threads[1].sliceGroup.slices[0];
      assert.strictEqual(taskSlice.args.my_object, foo15);
    }

    const events = [
      // An object.
      {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
      {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: { // @suppress longLineCheck
        snapshot: { x: {
          id: 'foo/0x1001',
          value: 'bar'
        }}}},
      {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck

      // A slice that references the object.
      {ts: 17000, pid: 1, tid: 1, ph: 'B', cat: 'c', name: 'taskSlice', args: {my_object: {id_ref: '0x1001'}}}, // @suppress longLineCheck
      {ts: 17500, pid: 1, tid: 1, ph: 'E', cat: 'c', name: 'taskSlice', args: {}} // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importArrayWithIDs', function() {
    function checkModel(m) {
      const p1 = m.processes[1];

      const sA = p1.objects.getSnapshotAt(new ScopedId('ptr', '0x1000'), 15);
      assert.isTrue(sA.args.x instanceof Array);
      assert.strictEqual(sA.args.x.length, 3);
      assert.isTrue(sA.args.x[0] instanceof tr.model.ObjectSnapshot);
      assert.isTrue(sA.args.x[1] instanceof tr.model.ObjectSnapshot);
      assert.isTrue(sA.args.x[2] instanceof tr.model.ObjectSnapshot);
    }

    const events = [
      {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: { // @suppress longLineCheck
        snapshot: { x: [
          {id: 'foo/0x1001', value: 'bar1'},
          {id: 'foo/0x1002', value: 'bar2'},
          {id: 'foo/0x1003', value: 'bar3'}
        ]}}}
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('importDoesNotMutateEventList', function() {
    const events = [
      // An object.
      {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
      {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: { // @suppress longLineCheck
        snapshot: {foo: 15}}},
      {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck

      // A slice that references the object.
      {ts: 17000, pid: 1, tid: 1, ph: 'B', cat: 'c', name: 'taskSlice', args: {
        my_object: {id_ref: '0x1000'}}
      },
      {ts: 17500, pid: 1, tid: 1, ph: 'E', cat: 'c', name: 'taskSlice', args: {}} // @suppress longLineCheck
    ];

    // The A type family exists to mutate the args list provided to
    // snapshots.
    function ASnapshot() {
      tr.model.ObjectSnapshot.apply(this, arguments);
      this.args.foo = 7;
    }
    ASnapshot.prototype = {
      __proto__: tr.model.ObjectSnapshot.prototype
    };

    // Import event while the A types are registered, causing the
    // arguments of the snapshots to be mutated.
    tr.model.ObjectSnapshot.subTypes.register(ASnapshot, {typeName: 'a'});
    const m = makeModel(events);
    tr.model.ObjectSnapshot.subTypes.unregister(ASnapshot);
    assert.isFalse(m.hasImportWarnings);

    // Verify that the events array wasn't modified.
    assert.deepEqual(
        events[1].args,
        {snapshot: {foo: 15}});
    assert.deepEqual(
        events[3].args,
        {my_object: {id_ref: '0x1000'}});
  });

  test('importFlowEvent', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];

      assert.isDefined(t);
      assert.strictEqual(m.flowEvents.length, 2);
      assert.strictEqual(m.flowIntervalTree.size, 2);

      const f0 = m.flowEvents[0];
      assert.strictEqual(f0.title, 'a');
      assert.strictEqual(f0.category, 'foo');
      assert.strictEqual(f0.id, 72);
      assert.closeTo(f0.start, 0.001, 1e-5);
      assert.closeTo(12 / 1000, f0.duration, 1e-5);
      assert.strictEqual(f0.startSlice.title, 'aSlice');
      assert.strictEqual(f0.endSlice.title, 'bSlice');

      // TODO(nduca): Replace this assertion with something better when
      // flow events don't create synthetic slices on their own.
      assert.isDefined(f0.startSlice);
      assert.isDefined(f0.endSlice);

      const f1 = m.flowEvents[1];
      assert.strictEqual(f1.title, f0.title);
      assert.strictEqual(f1.category, f0.category);
      assert.strictEqual(f1.id, f0.id);
      assert.closeTo(20 / 1000, f1.duration, 1e-5);

      assert.strictEqual(f1.startSlice.title, 'bSlice');
      assert.strictEqual(f1.endSlice.title, 'cSlice');

      assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
      assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
      assert.deepEqual(f1.startSlice.outFlowEvents, [f1]);
      assert.deepEqual(f1.endSlice.inFlowEvents, [f1]);
    }

    const events = [
      { name: 'aSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 547, ph: 'B', args: {} },  // @suppress longLineCheck
      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {} },  // @suppress longLineCheck
      { id: 72, pid: 52, tid: 53, ts: 549, ph: 'E', args: {} },  // @suppress longLineCheck

      { name: 'bSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 559, ph: 'B', args: {} },  // @suppress longLineCheck
      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 560, ph: 't', args: {} },  // @suppress longLineCheck
      { id: 72, pid: 52, tid: 53, ts: 561, ph: 'E', args: {} },  // @suppress longLineCheck

      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 580, ph: 'f', args: {} },   // @suppress longLineCheck
      { name: 'cSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 581, ph: 'B', args: {} },  // @suppress longLineCheck
      { id: 72, pid: 52, tid: 53, ts: 582, ph: 'E', args: {} }  // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  // Verify that flow events with identical ids but different (category, name)
  // pairs show up as different events.
  test('importFlowEventFullFlowId', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];

      assert.isDefined(t);
      assert.strictEqual(m.flowEvents.length, 4);
      assert.strictEqual(m.flowIntervalTree.size, 4);

      const f0 = m.flowEvents[0];
      assert.strictEqual(f0.title, 'a');
      assert.strictEqual(f0.category, 'foo');
      assert.strictEqual(f0.id, 72);
      assert.closeTo(f0.start, 0.001, 1e-5);
      assert.closeTo(12 / 1000, f0.duration, 1e-5);
      assert.strictEqual(f0.startSlice.title, 'aSlice');
      assert.strictEqual(f0.endSlice.title, 'bSlice');

      // TODO(nduca): Replace this assertion with something better when
      // flow events don't create synthetic slices on their own.
      assert.isDefined(f0.startSlice);
      assert.isDefined(f0.endSlice);

      const f1 = m.flowEvents[1];
      assert.strictEqual(f1.title, 'b');
      assert.strictEqual(f1.category, 'foo');
      assert.strictEqual(f1.id, 72);
      assert.closeTo(f1.start, 0.001, 1e-5);
      assert.closeTo(12 / 1000, f1.duration, 1e-5);
      assert.strictEqual(f1.startSlice.title, 'aSlice');
      assert.strictEqual(f1.endSlice.title, 'bSlice');

      assert.isDefined(f1.startSlice);
      assert.isDefined(f1.endSlice);

      const f2 = m.flowEvents[2];
      assert.strictEqual(f2.title, f0.title);
      assert.strictEqual(f2.category, f0.category);
      assert.strictEqual(f2.id, f0.id);
      assert.closeTo(20 / 1000, f2.duration, 1e-5);

      assert.strictEqual(f2.startSlice.title, 'bSlice');
      assert.strictEqual(f2.endSlice.title, 'cSlice');

      const f3 = m.flowEvents[3];
      assert.strictEqual(f3.title, f1.title);
      assert.strictEqual(f3.category, f1.category);
      assert.strictEqual(f3.id, f1.id);
      assert.closeTo(20 / 1000, f3.duration, 1e-5);

      assert.strictEqual(f3.startSlice.title, 'bSlice');
      assert.strictEqual(f3.endSlice.title, 'cSlice');

      assert.deepEqual(f0.startSlice.outFlowEvents, [f0, f1]);
      assert.deepEqual(f0.endSlice.inFlowEvents, [f0, f1]);
      assert.deepEqual(f2.startSlice.outFlowEvents, [f2, f3]);
      assert.deepEqual(f2.endSlice.inFlowEvents, [f2, f3]);

      assert.deepEqual(f1.startSlice.outFlowEvents, [f0, f1]);
      assert.deepEqual(f1.endSlice.inFlowEvents, [f0, f1]);
      assert.deepEqual(f3.startSlice.outFlowEvents, [f2, f3]);
      assert.deepEqual(f3.endSlice.inFlowEvents, [f2, f3]);
    }

    const events = [
      { name: 'aSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 547, ph: 'B', args: {} },  // @suppress longLineCheck
      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {} },  // @suppress longLineCheck
      { id: 72, pid: 52, tid: 53, ts: 549, ph: 'E', args: {} },  // @suppress longLineCheck
      { id: 72, pid: 52, tid: 53, ts: 549, ph: 'E', args: {} },  // @suppress longLineCheck
      { name: 'b', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {} },  // @suppress longLineCheck

      { name: 'bSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 559, ph: 'B', args: {} },  // @suppress longLineCheck
      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 560, ph: 't', args: {} },  // @suppress longLineCheck
      { name: 'b', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 560, ph: 't', args: {} },  // @suppress longLineCheck
      { id: 72, pid: 52, tid: 53, ts: 561, ph: 'E', args: {} },  // @suppress longLineCheck

      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 580, ph: 'f', args: {} },   // @suppress longLineCheck
      { name: 'b', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 580, ph: 'f', args: {} },   // @suppress longLineCheck
      { name: 'cSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 581, ph: 'B', args: {} },  // @suppress longLineCheck
      { id: 72, pid: 52, tid: 53, ts: 582, ph: 'E', args: {} }  // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importOldFlowEventBindtoNext', function() {
    function checkModel(m) {
      assert.strictEqual(m.flowEvents.length, 1);

      const f0 = m.flowEvents[0];

      assert.strictEqual(f0.title, 'flow');
      assert.strictEqual(f0.category, 'foo');
      assert.strictEqual(f0.id, 72);
      assert.strictEqual(f0.start, .548);
      assert.closeTo(32 / 1000, f0.duration, 1e-5);
      assert.strictEqual(f0.startSlice.title, 'slice1');
      assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
      assert.strictEqual(f0.endSlice.title, 'slice3');
      assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
    }

    // Old trace format without event.bp, and event.cat doesn't contain input
    const events = [
      { name: 'slice1', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100},   // @suppress longLineCheck
      { name: 'flow', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {}},  // @suppress longLineCheck

      { name: 'flow', cat: 'foo', id: 72, pid: 70, tid: 71, ts: 580, ph: 'f', args: {}},   // @suppress longLineCheck
      { name: 'slice2', cat: 'foo', pid: 70, tid: 71, ts: 570, ph: 'X', args: {}, dur: 100},   // @suppress longLineCheck
      { name: 'slice3', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', args: {}, dur: 1000}   // @suppress longLineCheck

    ];
    checkParsedAndStreamInput(events, checkModel, false, false);
  });

  test('importOldInputFlowEventBindtoParent', function() {
    function checkModel(m) {
      assert.strictEqual(m.flowEvents.length, 1);

      const f0 = m.flowEvents[0];

      assert.strictEqual(f0.title, 'flow');
      assert.strictEqual(f0.category, 'input');
      assert.strictEqual(f0.id, 72);
      assert.strictEqual(f0.start, .548);
      assert.closeTo(32 / 1000, f0.duration, 1e-5);
      assert.strictEqual(f0.startSlice.title, 'slice1');
      assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
      assert.strictEqual(f0.endSlice.title, 'slice2');
      assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
    }

    // Old trace format without event.bp, but event.cat contains input
    const events = [
      { name: 'slice1', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100},   // @suppress longLineCheck
      { name: 'flow', cat: 'input', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {}},  // @suppress longLineCheck

      { name: 'flow', cat: 'input', id: 72, pid: 70, tid: 71, ts: 580, ph: 'f', args: {}},   // @suppress longLineCheck
      { name: 'slice2', cat: 'foo', pid: 70, tid: 71, ts: 570, ph: 'X', args: {}, dur: 100},   // @suppress longLineCheck
      { name: 'slice3', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', args: {}, dur: 1000}   // @suppress longLineCheck

    ];
    checkParsedAndStreamInput(events, checkModel, false, false);
  });

  test('importOldIPCFlowEventBindtoParent', function() {
    function checkModel(m) {
      assert.strictEqual(m.flowEvents.length, 1);

      const f0 = m.flowEvents[0];

      assert.strictEqual(f0.title, 'flow');
      assert.strictEqual(f0.category, 'disabled-by-default-ipc.flow');
      assert.strictEqual(f0.id, 72);
      assert.strictEqual(f0.start, .548);
      assert.closeTo(32 / 1000, f0.duration, 1e-5);
      assert.strictEqual(f0.startSlice.title, 'slice1');
      assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
      assert.strictEqual(f0.endSlice.title, 'slice2');
      assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
    }

    // Old trace format without event.bp, but event.cat contains ipc.flow
    const events = [
      { name: 'slice1', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100},   // @suppress longLineCheck
      { name: 'flow', cat: 'disabled-by-default-ipc.flow', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {}},  // @suppress longLineCheck

      { name: 'flow', cat: 'disabled-by-default-ipc.flow', id: 72, pid: 70, tid: 71, ts: 580, ph: 'f', args: {}},   // @suppress longLineCheck
      { name: 'slice2', cat: 'foo', pid: 70, tid: 71, ts: 570, ph: 'X', args: {}, dur: 100},   // @suppress longLineCheck
      { name: 'slice3', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', args: {}, dur: 1000}   // @suppress longLineCheck

    ];
    checkParsedAndStreamInput(events, checkModel, false, false);
  });

  test('importNewFlowEventBindtoParent', function() {
    function checkModel(m) {
      assert.strictEqual(m.flowEvents.length, 1);

      const f0 = m.flowEvents[0];

      assert.strictEqual(f0.title, 'flow');
      assert.strictEqual(f0.category, 'foo');
      assert.strictEqual(f0.id, 72);
      assert.strictEqual(f0.start, .548);
      assert.closeTo(32 / 1000, f0.duration, 1e-5);
      assert.strictEqual(f0.startSlice.title, 'slice1');
      assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
      assert.strictEqual(f0.endSlice.title, 'slice2');
      assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
    }

    // New trace format with event.bp
    const events = [
      { name: 'slice1', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100},   // @suppress longLineCheck
      { name: 'flow', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', bp: 'e', args: {}},  // @suppress longLineCheck

      { name: 'flow', cat: 'foo', id: 72, pid: 70, tid: 71, ts: 580, ph: 'f', bp: 'e', args: {}},   // @suppress longLineCheck
      { name: 'slice2', cat: 'foo', pid: 70, tid: 71, ts: 570, ph: 'X', args: {}, dur: 100},   // @suppress longLineCheck
      { name: 'slice3', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', args: {}, dur: 1000}   // @suppress longLineCheck

    ];
    checkParsedAndStreamInput(events, checkModel, false, false);
  });

  test('importNewFlowEventWithInvalidBindingPoint', function() {
    function checkModel(m) {
      assert.strictEqual(m.flowEvents.length, 0);
    }

    // New trace format with event.bp, which however !== 'e'
    const events = [
      { name: 'slice1', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100},   // @suppress longLineCheck
      { name: 'flow', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', bp: 'z', args: {}},  // @suppress longLineCheck

      { name: 'flow', cat: 'foo', id: 72, pid: 70, tid: 71, ts: 580, ph: 'f', bp: 'z', args: {}},   // @suppress longLineCheck
      { name: 'slice2', cat: 'foo', pid: 70, tid: 71, ts: 570, ph: 'X', args: {}, dur: 100},   // @suppress longLineCheck
      { name: 'slice3', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', args: {}, dur: 1000}   // @suppress longLineCheck

    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importFlowV2OnePair', function() {
    function checkModel(m) {
      assert.strictEqual(m.flowEvents.length, 1);

      const f0 = m.flowEvents[0];

      assert.strictEqual(f0.startSlice.title, 'producer');
      assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
      assert.strictEqual(f0.endSlice.title, 'consumer');
      assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
    }

    // Flow V2: one flow producer one flow consumer
    const events = [
      { name: 'producer', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100, bind_id: '0xaaa', flow_out: true},   // @suppress longLineCheck
      { name: 'consumer', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', dur: 1000, bind_id: '0xaaa', flow_in: true}   // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importFlowV2OneFlowStep', function() {
    function checkModel(m) {
      assert.strictEqual(m.flowEvents.length, 2);

      const f0 = m.flowEvents[0];
      const f1 = m.flowEvents[1];

      assert.strictEqual(f0.startSlice.title, 'producer');
      assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
      assert.strictEqual(f0.endSlice.title, 'step');
      assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);

      assert.strictEqual(f1.startSlice.title, 'step');
      assert.deepEqual(f1.startSlice.outFlowEvents, [f1]);
      assert.strictEqual(f1.endSlice.title, 'consumer');
      assert.deepEqual(f1.endSlice.inFlowEvents, [f1]);
    }

    // Flow V2: one flow producer one flow consumer
    const events = [
      { name: 'producer', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100, bind_id: '0xaaa', flow_out: true},   // @suppress longLineCheck
      { name: 'step', cat: 'foo', pid: 62, tid: 63, ts: 647, ph: 'X', dur: 100, bind_id: '0xaaa', flow_out: true, flow_in: true},   // @suppress longLineCheck
      { name: 'consumer', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', dur: 1000, bind_id: '0xaaa', args: { 'queue_duration': 0}, flow_in: true}   // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importFlowV2MultipleConsumers', function() {
    function checkModel(m) {
      assert.strictEqual(m.flowEvents.length, 2);

      const f0 = m.flowEvents[0];
      const f1 = m.flowEvents[1];

      assert.strictEqual(f0.startSlice.title, 'producer');
      assert.strictEqual(f1.startSlice.title, 'producer');

      assert.strictEqual(f0.startSlice.outFlowEvents.length, 2);
      assert.deepEqual(f0.startSlice.outFlowEvents, [f0, f1]);
      assert.deepEqual(f1.startSlice.outFlowEvents, [f0, f1]);

      assert.strictEqual(f0.endSlice.title, 'consumer1');
      assert.strictEqual(f1.endSlice.title, 'consumer2');
      assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);
      assert.deepEqual(f1.endSlice.inFlowEvents, [f1]);
    }

    // Flow V2: one flow producer multiple flow consumers
    const events = [
      { name: 'producer', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100, bind_id: '0xaaa', flow_out: true},   // @suppress longLineCheck
      { name: 'consumer1', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', dur: 1000, bind_id: '0xaaa', flow_in: true},   // @suppress longLineCheck
      { name: 'consumer2', cat: 'foo', pid: 70, tid: 72, ts: 870, ph: 'X', dur: 1000, bind_id: '0xaaa', flow_in: true}   // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importFlowV2MultipleProducers', function() {
    function checkModel(m) {
      assert.strictEqual(m.flowEvents.length, 1);
    }

    // Flow V2: multiple flow producers, which is not allowed
    const events = [
      { name: 'producer1', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100, bind_id: '0xaaa', flow_out: true},   // @suppress longLineCheck
      { name: 'producer2', cat: 'foo', pid: 52, tid: 54, ts: 567, ph: 'X', dur: 100, bind_id: '0xaaa', flow_out: true},   // @suppress longLineCheck
      { name: 'consumer', cat: 'foo', pid: 70, tid: 71, ts: 770, ph: 'X', dur: 1000, bind_id: '0xaaa', flow_in: true}   // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  // This test creates a flow event that stops on the same timestamp that
  // the 'X' event which it triggers begins.
  test('importFlowEventOverlaps', function() {
    function checkModel(m) {
      const startT = m.processes[52].threads[53];
      const endT = m.processes[70].threads[71];

      assert.isDefined(startT);
      assert.strictEqual(startT.sliceGroup.slices.length, 1);

      assert.isDefined(endT);
      assert.strictEqual(endT.sliceGroup.slices.length, 1);

      assert.strictEqual(m.flowEvents.length, 1);

      // f0 represents 's' to 'f'
      const f0 = m.flowEvents[0];

      assert.strictEqual(f0.title, 'PostTask');
      assert.strictEqual(f0.category, 'foo');
      assert.strictEqual(f0.id, 72);
      assert.strictEqual(f0.start, .548);
      assert.closeTo(32 / 1000, f0.duration, 1e-5);
      assert.strictEqual(f0.startSlice.title, 'SomeTask');
      assert.deepEqual(f0.startSlice.outFlowEvents, [f0]);
      assert.strictEqual(f0.endSlice.title, 'RunTask');
      assert.deepEqual(f0.endSlice.inFlowEvents, [f0]);

      // TODO(nduca): Add assertions about the flow slices, esp that they were
      // found correctly.
    }

    const events = [
      { name: 'SomeTask', cat: 'foo', pid: 52, tid: 53, ts: 547, ph: 'X', dur: 100},   // @suppress longLineCheck
      { name: 'PostTask', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {}},  // @suppress longLineCheck

      { name: 'PostTask', cat: 'foo', id: 72, pid: 70, tid: 71, ts: 580, ph: 'f', args: { 'queue_duration': 0}},   // @suppress longLineCheck
      // Note that RunTask has the same time-stamp as PostTask 'f'
      { name: 'RunTask', cat: 'foo', pid: 70, tid: 71, ts: 580, ph: 'X', args: {'src_func': 'PostRunTask'}, dur: 1000}   // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('importOutOfOrderFlowEvent', function() {
    function checkModel(m) {
      const expected = [0.4, 0.0, 0.412];
      assert.strictEqual(m.flowIntervalTree.size, 3);

      const order = m.flowEvents.map(function(x) { return x.start; });
      for (let i = 0; i < expected.length; ++i) {
        assert.closeTo(expected[i], order[i], 1e-5);
      }
    }

    const events = [
      { name: 'SomeTask', cat: 'foo', pid: 52, tid: 53, ts: 548, ph: 'X', dur: 10},   // @suppress longLineCheck
      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {} },  // @suppress longLineCheck

      { name: 'SomeTask', cat: 'foo', pid: 52, tid: 53, ts: 148, ph: 'X', dur: 10},   // @suppress longLineCheck
      { name: 'b', cat: 'foo', id: 73, pid: 52, tid: 53, ts: 148, ph: 's', args: {} },  // @suppress longLineCheck

      { name: 'b', cat: 'foo', id: 73, pid: 52, tid: 53, ts: 570, ph: 'f', args: {} },   // @suppress longLineCheck
      { name: 'SomeTask', cat: 'foo', pid: 52, tid: 53, ts: 571, ph: 'X', dur: 10},   // @suppress longLineCheck

      { name: 'SomeTask', cat: 'foo', pid: 52, tid: 53, ts: 560, ph: 'X', dur: 10},   // @suppress longLineCheck
      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 560, ph: 't', args: {} },  // @suppress longLineCheck

      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 580, ph: 'f', args: {} },   // @suppress longLineCheck
      { name: 'SomeTask', cat: 'foo', pid: 52, tid: 53, ts: 581, ph: 'X', dur: 10}   // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importCompleteEvent', function() {
    function checkModel(m) {
      assert.strictEqual(m.numProcesses, 1);
      const p = m.processes[52];
      assert.isDefined(p);

      assert.strictEqual(p.numThreads, 1);
      const t = p.threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.sliceGroup.slices.length, 3);
      assert.strictEqual(t.tid, 53);

      let slice = t.sliceGroup.slices[0];
      assert.strictEqual(slice.title, 'a');
      assert.strictEqual(slice.category, 'baz');
      assert.closeTo(0, slice.start, 1e-5);
      assert.closeTo(1 / 1000, slice.duration, 1e-5);
      assert.strictEqual(slice.subSlices.length, 0);

      slice = t.sliceGroup.slices[1];
      assert.strictEqual(slice.title, 'b');
      assert.strictEqual(slice.category, 'foo');
      assert.closeTo((730 - 629) / 1000, slice.start, 1e-5);
      assert.closeTo(20 / 1000, slice.duration, 1e-5);
      assert.strictEqual(slice.subSlices.length, 1);

      slice = t.sliceGroup.slices[2];
      assert.strictEqual(slice.title, 'c');
      assert.isTrue(slice.didNotFinish);
      assert.closeTo(10 / 1000, slice.duration, 1e-5);
    }

    const events = [
      { name: 'a', args: {}, pid: 52, ts: 629, dur: 1, cat: 'baz', tid: 53, ph: 'X' },  // @suppress longLineCheck
      { name: 'b', args: {}, pid: 52, ts: 730, dur: 20, cat: 'foo', tid: 53, ph: 'X' },  // @suppress longLineCheck
      { name: 'c', args: {}, pid: 52, ts: 740, cat: 'baz', tid: 53, ph: 'X' }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importFlowEventsWithStackFrame', function() {
    function checkModel(m) {
      assert.strictEqual(m.flowEvents.length, 2);

      const f0 = m.flowEvents[0];
      assert.strictEqual(f0.startStackFrame.title, 'fn1');
      assert.strictEqual(f0.endStackFrame.title, 'fn2');

      const f1 = m.flowEvents[1];
      assert.strictEqual(f1.startStackFrame.title, 'fn2');
      assert.strictEqual(f1.endStackFrame.title, 'fn3');
    }

    const eventData = {
      traceEvents: [
        { name: 'aSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 547, ph: 'B', args: {} },  // @suppress longLineCheck
        { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {}, sf: 1 },  // @suppress longLineCheck
        { id: 72, pid: 52, tid: 53, ts: 549, ph: 'E', args: {} },  // @suppress longLineCheck

        { name: 'bSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 559, ph: 'B', args: {} },  // @suppress longLineCheck
        { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 560, ph: 't', args: {}, sf: 2 },  // @suppress longLineCheck
        { id: 72, pid: 52, tid: 53, ts: 561, ph: 'E', args: {} },  // @suppress longLineCheck

        { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 580, ph: 'f', args: {}, sf: 3 },   // @suppress longLineCheck
        { name: 'cSlice', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 581, ph: 'B', args: {} },  // @suppress longLineCheck
        { id: 72, pid: 52, tid: 53, ts: 582, ph: 'E', args: {} }  // @suppress longLineCheck
      ],
      stackFrames: {
        '1': {
          category: 'm1',
          name: 'fn1'
        },
        '2': {
          category: 'm2',
          name: 'fn2'
        },
        '3': {
          category: 'm3',
          name: 'fn3'
        }
      }
    };
    checkParsedAndStreamInput(eventData, checkModel);
  });

  test('importCompleteEventWithCpuDuration', function() {
    function checkModel(m) {
      assert.strictEqual(m.numProcesses, 1);
      const p = m.processes[52];
      assert.isDefined(p);

      assert.strictEqual(p.numThreads, 1);
      const t = p.threads[53];
      assert.isDefined(t);
      assert.strictEqual(t.sliceGroup.slices.length, 3);
      assert.strictEqual(t.tid, 53);

      let slice = t.sliceGroup.slices[0];
      assert.strictEqual(slice.title, 'a');
      assert.strictEqual(slice.category, 'baz');
      assert.closeTo(0, slice.start, 1e-5);
      assert.closeTo(1 / 1000, slice.duration, 1e-5);
      assert.closeTo(12 / 1000, slice.cpuStart, 1e-5);
      assert.closeTo(1 / 1000, slice.cpuDuration, 1e-5);
      assert.strictEqual(slice.subSlices.length, 0);

      slice = t.sliceGroup.slices[1];
      assert.strictEqual(slice.title, 'b');
      assert.strictEqual(slice.category, 'foo');
      assert.closeTo((730 - 629) / 1000, slice.start, 1e-5);
      assert.closeTo(20 / 1000, slice.duration, 1e-5);
      assert.closeTo(110 / 1000, slice.cpuStart, 1e-5);
      assert.closeTo(16 / 1000, slice.cpuDuration, 1e-5);
      assert.strictEqual(slice.subSlices.length, 1);

      slice = t.sliceGroup.slices[2];
      assert.strictEqual(slice.title, 'c');
      assert.isTrue(slice.didNotFinish);
      assert.closeTo(10 / 1000, slice.duration, 1e-5);
    }

    const events = [
      { name: 'a', args: {}, pid: 52, ts: 629, dur: 1, cat: 'baz', tid: 53, ph: 'X', tts: 12, tdur: 1 },  // @suppress longLineCheck
      { name: 'b', args: {}, pid: 52, ts: 730, dur: 20, cat: 'foo', tid: 53, ph: 'X', tts: 110, tdur: 16 },  // @suppress longLineCheck
      { name: 'c', args: {}, pid: 52, ts: 740, cat: 'baz', tid: 53, ph: 'X', tts: 115 }  // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importNestedCompleteEventWithTightBounds', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];

      const sA = findSliceNamed(t.sliceGroup, 'a');
      const sB = findSliceNamed(t.sliceGroup, 'b');

      assert.strictEqual(sA.title, 'a');
      assert.strictEqual(sA.category, 'baz');
      assert.strictEqual(sA.start, 244654227.065);
      assert.strictEqual(sA.duration, 36.075);
      assert.closeTo(0.03, sA.selfTime, 1e-5);

      assert.strictEqual(sB.title, 'b');
      assert.strictEqual(sB.category, 'foo');
      assert.strictEqual(sB.start, 244654227.095);
      assert.strictEqual(sB.duration, 36.045);

      assert.strictEqual(sA.subSlices.length, 1);
      assert.strictEqual(sA.subSlices[0], sB);
      assert.strictEqual(sB.parentSlice, sA);
    }

    const events = [
      { name: 'a', args: {}, pid: 52, ts: 244654227065, dur: 36075, cat: 'baz', tid: 53, ph: 'X' },  // @suppress longLineCheck
      { name: 'b', args: {}, pid: 52, ts: 244654227095, dur: 36045, cat: 'foo', tid: 53, ph: 'X' }  // @suppress longLineCheck
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });


  test('importCompleteEventWithStackFrame', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[2];
      assert.isDefined(t);
      assert.strictEqual(t.sliceGroup.slices.length, 2);

      const s0 = t.sliceGroup.slices[0];
      assert.strictEqual(s0.startStackFrame.title, 'frame7');
      assert.isUndefined(s0.endStackFrame);

      const s1 = t.sliceGroup.slices[1];
      assert.strictEqual(s1.startStackFrame.title, 'frame8');
      assert.strictEqual(s1.endStackFrame.title, 'frame9');
    }

    const eventData = {
      traceEvents: [
        { name: 'a', args: {}, pid: 1, ts: 0, dur: 1, cat: 'baz', tid: 2, ph: 'X', sf: 7 }, // @suppress longLineCheck
        { name: 'b', args: {}, pid: 1, ts: 5, dur: 1, cat: 'baz', tid: 2, ph: 'X', sf: 8, esf: 9 } // @suppress longLineCheck
      ],
      stackFrames: {
        '1': {
          category: 'm1',
          name: 'main'
        },
        '7': {
          category: 'm2',
          name: 'frame7',
          parent: '1'
        },
        '8': {
          category: 'm2',
          name: 'frame8',
          parent: '1'
        },
        '9': {
          category: 'm2',
          name: 'frame9',
          parent: '1'
        }
      }
    };
    checkParsedAndStreamInput(eventData, checkModel);
  });

  test('importAsyncEventWithSameTimestamp', function() {
    function checkModel(m) {
      const t = m.processes[52].threads[53];

      assert.strictEqual(t.asyncSliceGroup.slices.length, 1);
      const parentSlice = t.asyncSliceGroup.slices[0];
      assert.strictEqual(parentSlice.title, 'a');
      assert.strictEqual(parentSlice.category, 'foo');
      assert.isTrue(parentSlice.isTopLevel);

      assert.isDefined(parentSlice.subSlices);
      const subSlices = parentSlice.subSlices;
      assert.strictEqual(subSlices.length, 1000);
      // Slices should be sorted according to 'ts'. And if 'ts' is the same,
      // slices should keep the order that they were recorded.
      for (let i = 0; i < 1000; i++) {
        assert.strictEqual(i + 1, subSlices[i].args.seq);
        assert.isFalse(subSlices[i].isTopLevel);
      }
    }

    const events = [];
    // Events are added with ts 0, 1, 1, 2, 2, 3, 3 ...500, 500, 1000
    // and use 'seq' to track the order of when the event is recorded.
    events.push({name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 0, ph: 'S', args: {'seq': 0}});  // @suppress longLineCheck

    for (let i = 1; i <= 1000; i++) {
      events.push({name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: Math.round(i / 2), ph: 'T', args: {'seq': i}});  // @suppress longLineCheck
    }

    events.push({name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 1000, ph: 'F', args: {'seq': 1001}});  // @suppress longLineCheck
    checkParsedAndStreamInput(events, checkModel);
  });

  test('sampleDataSimple', function() {
    function checkModel(m) {
      assert.isDefined(m.kernel.cpus[0]);
      assert.strictEqual(m.getAllThreads().length, 1);

      assert.strictEqual(m.samples.length, 3);

      const t1 = m.processes[1].threads[1];
      assert.strictEqual(t1.samples.length, 3);

      const c0 = m.kernel.cpus[0];
      const c1 = m.kernel.cpus[1];
      assert.strictEqual(c0.samples.length, 2);
      assert.strictEqual(c1.samples.length, 1);

      assert.strictEqual(m.samples[0].cpu, c0);
      assert.strictEqual(m.samples[0].thread, t1);
      assert.strictEqual(m.samples[0].title, 'cycles:HG');
      assert.strictEqual(m.samples[0].start, 1);
      assert.deepEqual(
          ['a_sub', 'a', 'main'],
          m.samples[0].userFriendlyStack
              .map(x => x.substr(0, x.indexOf('url') - 1)));
      assert.strictEqual(m.samples[0].weight, 1);
    }

    const events = {
      'traceEvents': [],
      'stackFrames': {
        '1': {
          'category': 'mod',
          'name': 'main'
        },
        '2': {
          'category': 'mod',
          'name': 'a',
          'parent': 1
        },
        '3': {
          'category': 'mod',
          'name': 'a_sub',
          'parent': 2
        },
        '4': {
          'category': 'mod',
          'name': 'b',
          'parent': 1
        }
      },
      'samples': [
        {
          'cpu': 0, 'tid': 1, 'ts': 1000.0,
          'name': 'cycles:HG', 'sf': 3, 'weight': 1
        },
        {
          'cpu': 0, 'tid': 1, 'ts': 2000.0,
          'name': 'cycles:HG', 'sf': 2, 'weight': 1
        },
        {
          'cpu': 1, 'tid': 1, 'ts': 3000.0,
          'name': 'cycles:HG', 'sf': 3, 'weight': 1
        }
      ]
    };
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('importMemoryDumps_verifyProcessAndGlobalMemoryDumpLinks', function() {
    function checkModel(m) {
      const p1 = m.getProcess(42);
      const p2 = m.getProcess(43);
      assert.isDefined(p1);
      assert.isDefined(p2);

      // Check that Model and Process objects contain the right dumps.
      assert.strictEqual(m.globalMemoryDumps.length, 2);
      assert.strictEqual(p1.memoryDumps.length, 2);
      assert.strictEqual(p2.memoryDumps.length, 1);

      assert.strictEqual(m.globalMemoryDumps[0].start, 10);
      assert.strictEqual(p1.memoryDumps[0].start, 10);
      assert.strictEqual(p2.memoryDumps[0].start, 11);
      assert.strictEqual(m.globalMemoryDumps[0].duration, 1);
      assert.strictEqual(p1.memoryDumps[0].duration, 0);
      assert.strictEqual(p2.memoryDumps[0].duration, 0);

      assert.strictEqual(m.globalMemoryDumps[1].start, 13);
      assert.strictEqual(p1.memoryDumps[1].start, 13);
      assert.strictEqual(m.globalMemoryDumps[1].duration, 0);
      assert.strictEqual(p1.memoryDumps[1].duration, 0);

      // Check that GlobalMemoryDump and ProcessMemoryDump objects are
      // interconnected correctly.
      assert.strictEqual(p1.memoryDumps[0],
          m.globalMemoryDumps[0].processMemoryDumps[42]);
      assert.strictEqual(p2.memoryDumps[0],
          m.globalMemoryDumps[0].processMemoryDumps[43]);
      assert.strictEqual(
          p1.memoryDumps[0].globalMemoryDump, m.globalMemoryDumps[0]);
      assert.strictEqual(
          p2.memoryDumps[0].globalMemoryDump, m.globalMemoryDumps[0]);

      assert.strictEqual(p1.memoryDumps[1],
          m.globalMemoryDumps[1].processMemoryDumps[42]);
      assert.strictEqual(
          p1.memoryDumps[1].globalMemoryDump, m.globalMemoryDumps[1]);
    }

    const events = [
      // 2 process memory dump events.
      {
        name: 'a',
        pid: 42,
        ts: 10000,
        cat: 'test',
        tid: 53,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '100'
            }
          }
        }
      },
      {
        name: 'b',
        pid: 43,
        ts: 11000,
        cat: 'test',
        tid: 54,
        ph: 'v',
        id: '0x0001',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '200'
            }
          }
        }
      },
      // 1 process memory dump event.
      {
        name: 'd',
        pid: 42,
        ts: 13000,
        cat: 'test',
        tid: 56,
        ph: 'v',
        id: '0xfffffff12345678',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '300'
            }
          }
        }
      }
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('importMemoryDumps_totalResidentBytesOnly', function() {
    function checkModel(m) {
      const p = m.getProcess(42);
      const d = p.memoryDumps[0];

      assert.strictEqual(d.totals.residentBytes, 9007199254740991);
      assert.isUndefined(d.totals.peakResidentBytes);
      assert.isUndefined(d.totals.arePeakResidentBytesResettable);
      assert.isUndefined(d.totals.platformSpecific);
      assert.isUndefined(d.mostRecentVmRegions);
      assert.lengthOf(d.memoryAllocatorDumps, 0);
    }

    const events = [
      {
        pid: 42,
        ts: 10,
        ph: 'v',
        id: '0x01',
        cat: 'memory-infra',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '1fffffffffffff'
            }
          }
        }
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importMemoryDumps_withPeakResidentBytes', function() {
    function checkModel(m) {
      const p = m.getProcess(42);
      const d = p.memoryDumps[0];

      assert.strictEqual(d.totals.residentBytes, 9007199254740991);
      assert.strictEqual(d.totals.peakResidentBytes, 13510798882111488);
      assert.isTrue(d.totals.arePeakResidentBytesResettable);
      assert.isUndefined(d.totals.platformSpecific);
      assert.isUndefined(d.mostRecentVmRegions);
      assert.lengthOf(d.memoryAllocatorDumps, 0);
    }

    const events = [
      {
        pid: 42,
        ts: 10,
        ph: 'v',
        id: '0x01',
        cat: 'memory-infra',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '1fffffffffffff',
              peak_resident_set_bytes: '2fffffffffffff',
              is_peak_rss_resetable: true
            }
          }
        }
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importMemoryDumps_platformSpecificTotals', function() {
    function checkModel(m) {
      const p = m.getProcess(42);
      const d = p.memoryDumps[0];

      assert.strictEqual(d.totals.residentBytes, 9007199254740991);
      assert.isUndefined(d.totals.peakResidentBytes);
      assert.isUndefined(d.totals.arePeakResidentBytesResettable);
      assert.deepEqual(d.totals.platformSpecific,
          {private_bytes: 4503599627370495, shared_bytes: 4503599627370496});
      assert.isUndefined(d.mostRecentVmRegions);
      assert.lengthOf(d.memoryAllocatorDumps, 0);
    }

    const events = [
      {
        pid: 42,
        ts: 10,
        ph: 'v',
        id: '0x01',
        cat: 'memory-infra',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '1fffffffffffff',
              private_bytes: 'fffffffffffff',
              shared_bytes: '10000000000000'
            }
          }
        }
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importMemoryDumps_vmRegions', function() {
    function checkModel(m) {
      const p = m.getProcess(42);
      const d = p.memoryDumps[0];

      checkVMRegions(d.vmRegions, [
        {
          startAddress: 240,
          sizeInBytes: 336,
          protectionFlags: VMRegion.PROTECTION_FLAG_READ |
              VMRegion.PROTECTION_FLAG_WRITE,
          mappedFile: '[stack:20310]',
          byteStats: {
            privateCleanResident: 64,
            privateDirtyResident: 32,
            sharedCleanResident: 256,
            sharedDirtyResident: 0,
            proportionalResident: 158,
            swapped: 80
          }
        },
        {
          startAddress: 848,
          sizeInBytes: 592,
          protectionFlags: VMRegion.PROTECTION_FLAG_READ |
              VMRegion.PROTECTION_FLAG_EXECUTE,
          mappedFile: '/dev/ashmem/dalvik',
          byteStats: {
            proportionalResident: 205,
            privateDirtyResident: 205,
            swapped: 0
          }
        },
        {
          startAddress: 140673331539968,
          sizeInBytes: 262144,
          protectionFlags: VMRegion.PROTECTION_FLAG_READ |
                           VMRegion.PROTECTION_FLAG_WRITE |
                           VMRegion.PROTECTION_FLAG_MAYSHARE,
          mappedFile: '/run/shm/.org.chromium.Chromium.sqqN11 (deleted)',
          byteStats: {
            privateCleanResident: 0,
            privateDirtyResident: 262144,
            sharedCleanResident: 0,
            sharedDirtyResident: 0,
            proportionalResident: 262144,
            swapped: 0
          }
        }
      ]);

      assert.strictEqual(d.totals.residentBytes, 0);
      assert.isUndefined(d.totals.peakResidentBytes);
      assert.isUndefined(d.totals.arePeakResidentBytesResettable);
      assert.isUndefined(d.totals.platformSpecific);
      assert.lengthOf(d.memoryAllocatorDumps, 0);
    }

    const events = [
      {
        name: 'some_dump_name',
        pid: 42,
        ts: 10,
        cat: 'test',
        tid: 53,
        ph: 'v',
        id: '000',
        cat: 'memory-infra',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '0'
            },
            process_mmaps: {
              vm_regions: [
                {
                  sa: 'f0',
                  sz: '150',
                  pf: 6,
                  mf: '[stack:20310]',
                  bs: {
                    pss: '9e',
                    pc: '40',
                    pd: '20',
                    sc: '100',
                    sd: '0',
                    sw: '50'
                  }
                },
                {
                  sa: '350',
                  sz: '250',
                  pf: 5,
                  mf: '/dev/ashmem/dalvik',
                  bs: {
                    pss: 'cd',
                    pd: 'cd',
                    sc: undefined,
                    sw: '0'
                  }
                },
                {
                  sa: '7ff10ff4b000',
                  sz: '40000',
                  pf: 134,
                  mf: '/run/shm/.org.chromium.Chromium.sqqN11 (deleted)',
                  bs: {
                    pss: '40000',
                    pc: '0',
                    pd: '40000',
                    sc: '0',
                    sd: '0',
                    sw: '0'
                  }
                }
              ]
            }
          }
        }
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importMemoryDumps_explicitMemoryAllocatorDumps', function() {
    function checkModel(m) {
      const p = m.getProcess(42);
      const d = p.memoryDumps[0];

      assert.strictEqual(d.memoryAllocatorDumps.length, 2);

      const oilpanRoot = d.getMemoryAllocatorDumpByFullName('oilpan');
      const v8Root = d.getMemoryAllocatorDumpByFullName('v8');
      assert.isDefined(oilpanRoot);
      assert.isDefined(v8Root);
      assert.include(d.memoryAllocatorDumps, oilpanRoot);
      assert.include(d.memoryAllocatorDumps, v8Root);

      checkDumpNumericsAndDiagnostics(oilpanRoot, {
        'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 47),
        'size': 32768,
        'effective_size': 32768,
        'inner_size': 4096
      }, {});
      assert.strictEqual(oilpanRoot.children.length, 2);

      const oilpanBucket1 = d.getMemoryAllocatorDumpByFullName(
          'oilpan/heap2/bucket1');
      assert.isDefined(oilpanBucket1);
      assert.strictEqual(oilpanBucket1.fullName, 'oilpan/heap2/bucket1');
      assert.strictEqual(oilpanBucket1.name, 'bucket1');
      checkDumpNumericsAndDiagnostics(oilpanBucket1, {
        'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 31),
        'size': 8192,
        'effective_size': 8192,
        'inner_size': 8192
      }, {});
      assert.strictEqual(oilpanBucket1.children.length, 0);

      assert.isDefined(oilpanBucket1.parent);
      assert.strictEqual(oilpanBucket1.parent.fullName, 'oilpan/heap2');
      assert.strictEqual(oilpanBucket1.parent.name, 'heap2');
      assert.include(oilpanBucket1.parent.children, oilpanBucket1);

      assert.isDefined(oilpanBucket1.parent.parent);
      assert.strictEqual(oilpanBucket1.parent.parent, oilpanRoot);

      assert.strictEqual(d.totals.residentBytes, 256);
      assert.isUndefined(d.totals.peakResidentBytes);
      assert.isUndefined(d.totals.arePeakResidentBytesResettable);
      assert.isUndefined(d.totals.platformSpecific);
      assert.isUndefined(d.mostRecentVmRegions);
    }

    const events = [
      {
        name: 'a',
        pid: 42,
        ts: 10,
        cat: 'test',
        tid: 53,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '100'
            },
            allocators: {
              'oilpan': {
                guid: '1a',
                attrs: {
                  objects_count: {
                    type: 'scalar', units: 'objects', value: '2f'
                  },
                  inner_size: {type: 'scalar', units: 'bytes', value: '1000'},
                  size: {type: 'scalar', units: 'bytes', value: '8000'}
                }
              },
              'oilpan/heap1': {
                guid: '2b',
                attrs: {
                  objects_count: {
                    type: 'scalar', units: 'objects', value: '3f'
                  },
                  inner_size: {type: 'scalar', units: 'bytes', value: '3000'},
                  size: {type: 'scalar', units: 'bytes', value: '4000'}
                }
              },
              'oilpan/heap2': {
                guid: '3c',
                attrs: {
                  objects_count: {
                    type: 'scalar', units: 'objects', value: '4f'
                  },
                  inner_size: {type: 'scalar', units: 'bytes', value: '4000'},
                  size: {type: 'scalar', units: 'bytes', value: '4000'}
                }
              },
              'oilpan/heap2/bucket1': {
                // Deliberately missing GUID (to check that the importer does
                // not skip memory allocator dump without GUID).
                attrs: {
                  objects_count: {
                    type: 'scalar', units: 'objects', value: '1f'
                  },
                  inner_size: {type: 'scalar', units: 'bytes', value: '2000'},
                  size: {type: 'scalar', units: 'bytes', value: '2000'}
                }
              },
              'v8': {
                guid: '5e',
                attrs: {
                  objects_count: {
                    type: 'scalar', units: 'objects', value: '5f'
                  },
                  inner_size: {type: 'scalar', units: 'bytes', value: '5000'},
                  size: {type: 'scalar', units: 'bytes', value: '6000'}
                }
              }
            }
          }
        }
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importMemoryDumps_implicitMemoryAllocatorDumps', function() {
    function checkModel(m) {
      const p = m.getProcess(42);
      const d = p.memoryDumps[0];

      assert.strictEqual(d.memoryAllocatorDumps.length, 2);

      const oilpanRoot = d.getMemoryAllocatorDumpByFullName('oilpan');
      const v8Root = d.getMemoryAllocatorDumpByFullName('v8');
      assert.isDefined(oilpanRoot);
      assert.isDefined(v8Root);
      assert.include(d.memoryAllocatorDumps, oilpanRoot);
      assert.include(d.memoryAllocatorDumps, v8Root);

      checkDumpNumericsAndDiagnostics(oilpanRoot, {
        'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 94),
        'size': 24576,
        'effective_size': 24576,
        'inner_size': 20480
      }, {});
      assert.strictEqual(oilpanRoot.children.length, 2);

      const oilpanBucket1 = d.getMemoryAllocatorDumpByFullName(
          'oilpan/heap2/bucket1');
      assert.isDefined(oilpanBucket1);
      assert.strictEqual(oilpanBucket1.fullName, 'oilpan/heap2/bucket1');
      assert.strictEqual(oilpanBucket1.name, 'bucket1');
      checkDumpNumericsAndDiagnostics(oilpanBucket1, {
        'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 31),
        'size': 8192,
        'effective_size': 8192,
        'inner_size': 8192
      }, {});
      assert.strictEqual(oilpanBucket1.children.length, 0);

      assert.isDefined(oilpanBucket1.parent);
      assert.strictEqual(oilpanBucket1.parent.fullName, 'oilpan/heap2');
      assert.strictEqual(oilpanBucket1.parent.name, 'heap2');
      assert.include(oilpanBucket1.parent.children, oilpanBucket1);

      assert.isDefined(oilpanBucket1.parent.parent);
      assert.strictEqual(oilpanBucket1.parent.parent, oilpanRoot);

      assert.strictEqual(d.totals.residentBytes, 256);
      assert.isUndefined(d.totals.peakResidentBytes);
      assert.isUndefined(d.totals.arePeakResidentBytesResettable);
      assert.isUndefined(d.totals.platformSpecific);
      assert.isUndefined(d.mostRecentVmRegions);
    }

    const events = [
      {
        name: 'a',
        pid: 42,
        ts: 10,
        cat: 'test',
        tid: 53,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '100'
            },
            allocators: {
              'oilpan/heap1': {
                guid: '999',
                attrs: {
                  objects_count: {
                    type: 'scalar', units: 'objects', value: '3f'
                  },
                  inner_size: {type: 'scalar', units: 'bytes', value: '3000'},
                  size: {type: 'scalar', units: 'bytes', value: '4000'}
                }
              },
              'oilpan/heap2/bucket1': {
                guid: '888',
                attrs: {
                  objects_count: {
                    type: 'scalar', units: 'objects', value: '1f'
                  },
                  inner_size: {type: 'scalar', units: 'bytes', value: '2000'},
                  size: {type: 'scalar', units: 'bytes', value: '2000'}
                }
              },
              'v8': {
                guid: '777',
                attrs: {
                  objects_count: {
                    type: 'scalar', units: 'objects', value: '5f'
                  },
                  inner_size: {type: 'scalar', units: 'bytes', value: '5000'},
                  size: {type: 'scalar', units: 'bytes', value: '6000'}
                }
              }
            }
          }
        }
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importMemoryDumps_globalMemoryAllocatorDumps', function() {
    function checkModel(m) {
      const p = m.getProcess(42);
      const gmd = m.globalMemoryDumps[0];
      const pmd = p.memoryDumps[0];

      assert.isUndefined(gmd.totals);
      assert.strictEqual(pmd.totals.residentBytes, 256);
      assert.isUndefined(pmd.totals.peakResidentBytes);
      assert.isUndefined(pmd.totals.arePeakResidentBytesResettable);
      assert.isUndefined(pmd.totals.platformSpecific);

      assert.isUndefined(gmd.mostRecentVmRegions);
      assert.isUndefined(pmd.mostRecentVmRegions);

      assert.strictEqual(gmd.memoryAllocatorDumps.length, 1);
      assert.strictEqual(pmd.memoryAllocatorDumps.length, 1);

      // Global memory allocator dumps.
      const sharedBitmapManager = gmd.getMemoryAllocatorDumpByFullName(
          'shared_bitmap_manager');
      assert.isDefined(sharedBitmapManager);
      assert.include(gmd.memoryAllocatorDumps, sharedBitmapManager);

      checkDumpNumericsAndDiagnostics(sharedBitmapManager, {
        'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 31),
        'size': 8192,
        'effective_size': 8192,
        'inner_size': 8192
      }, {});
      assert.lengthOf(sharedBitmapManager.children, 1);

      const bitmap2 = gmd.getMemoryAllocatorDumpByFullName(
          'shared_bitmap_manager/bitmap2');
      assert.isDefined(bitmap2);
      assert.include(sharedBitmapManager.children, bitmap2);
      assert.strictEqual(bitmap2.parent, sharedBitmapManager);

      checkDumpNumericsAndDiagnostics(bitmap2, {
        'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 31),
        'size': 8192,
        'effective_size': 8192,
        'inner_size': 8192
      }, { 'weather': 'sunny' });
      assert.lengthOf(bitmap2.children, 0);

      assert.isUndefined(gmd.getMemoryAllocatorDumpByFullName('tile_manager'));
      assert.isUndefined(
          gmd.getMemoryAllocatorDumpByFullName('tile_manager/tile1'));

      // Process memory allocator dumps.
      const tileManagerRoot = pmd.getMemoryAllocatorDumpByFullName(
          'tile_manager');
      assert.isDefined(tileManagerRoot);
      assert.include(pmd.memoryAllocatorDumps, tileManagerRoot);
      assert.isUndefined(tileManagerRoot.parent);

      checkDumpNumericsAndDiagnostics(tileManagerRoot, {
        'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 63),
        'size': 16384,
        'effective_size': 16384,
        'inner_size': 12288
      }, {});
      assert.lengthOf(tileManagerRoot.children, 1);

      const tile1 = pmd.getMemoryAllocatorDumpByFullName(
          'tile_manager/tile1');
      assert.isDefined(tile1);
      assert.include(tileManagerRoot.children, tile1);
      assert.strictEqual(tile1.parent, tileManagerRoot);

      checkDumpNumericsAndDiagnostics(tile1, {
        'objects_count': new Scalar(unitlessNumber_smallerIsBetter, 63),
        'size': 16384,
        'effective_size': 16384,
        'inner_size': 12288
      }, { 'weather': 'rainy' });
      assert.lengthOf(tile1.children, 0);

      assert.isUndefined(
          pmd.getMemoryAllocatorDumpByFullName('shared_bitmap_manager'));
      assert.isUndefined(
          pmd.getMemoryAllocatorDumpByFullName(
              'shared_bitmap_manager/bitmap2'));
    }

    const events = [
      {
        name: 'a',
        pid: 42,
        ts: 10,
        cat: 'test',
        tid: 53,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '100'
            },
            allocators: {
              'tile_manager/tile1': {
                guid: '21',
                attrs: {
                  objects_count: {
                    type: 'scalar', units: 'objects', value: '3f'
                  },
                  inner_size: {type: 'scalar', units: 'bytes', value: '3000'},
                  size: {type: 'scalar', units: 'bytes', value: '4000'},
                  weather: {type: 'string', units: '', value: 'rainy'}
                }
              },
              'global/shared_bitmap_manager/bitmap2': {
                guid: '42',
                attrs: {
                  objects_count: {
                    type: 'scalar', units: 'objects', value: '1f'
                  },
                  inner_size: {type: 'scalar', units: 'bytes', value: '2000'},
                  size: {type: 'scalar', units: 'bytes', value: '2000'},
                  weather: {type: 'string', units: '', value: 'sunny'}
                }
              }
            }
          }
        }
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importMemoryDumps_memoryAllocatorDumpEdges', function() {
    function checkModel(model) {
      const browserProcess = model.getProcess(42);
      const rendererProcess = model.getProcess(43);
      const gpuProcess = model.getProcess(44);

      assert.lengthOf(model.globalMemoryDumps, 1);
      assert.lengthOf(browserProcess.memoryDumps, 1);
      assert.lengthOf(rendererProcess.memoryDumps, 1);
      assert.lengthOf(gpuProcess.memoryDumps, 1);

      const globalDump = model.globalMemoryDumps[0];
      const browserDump = browserProcess.memoryDumps[0];
      const rendererDump = rendererProcess.memoryDumps[0];
      const gpuDump = gpuProcess.memoryDumps[0];

      // Global memory allocator dump.
      assert.lengthOf(globalDump.memoryAllocatorDumps, 1);

      const globalDumpShared = globalDump.getMemoryAllocatorDumpByFullName(
          'shared');
      assert.isDefined(globalDumpShared);
      assert.include(globalDump.memoryAllocatorDumps, globalDumpShared);
      checkDumpNumericsAndDiagnostics(globalDumpShared, {
        'area': new Scalar(unitlessNumber_smallerIsBetter, 9)
      }, { 'color': 'blue' });
      assert.lengthOf(globalDumpShared.children, 0);
      assert.isUndefined(globalDumpShared.parent);

      assert.isUndefined(globalDumpShared.owns);
      assert.lengthOf(globalDumpShared.ownedBy, 3);
      assert.lengthOf(globalDumpShared.retains, 0);
      assert.lengthOf(globalDumpShared.retainedBy, 0);

      // Browser memory allocator dump.
      assert.lengthOf(browserDump.memoryAllocatorDumps, 1);

      const browserDumpLocal = browserDump.getMemoryAllocatorDumpByFullName(
          'local');
      assert.isDefined(browserDumpLocal);
      assert.include(browserDump.memoryAllocatorDumps, browserDumpLocal);
      checkDumpNumericsAndDiagnostics(browserDumpLocal, {
        'area': new Scalar(unitlessNumber_smallerIsBetter, 9)
      }, { 'color': 'blue', 'mood': 'very good' });
      assert.lengthOf(browserDumpLocal.children, 0);
      assert.isUndefined(browserDumpLocal.parent);

      assert.isDefined(browserDumpLocal.owns);
      assert.lengthOf(browserDumpLocal.ownedBy, 0);
      assert.lengthOf(browserDumpLocal.retains, 0);
      assert.lengthOf(browserDumpLocal.retainedBy, 0);

      const browserDumpLocalOwnsLink = browserDumpLocal.owns;
      assert.include(globalDumpShared.ownedBy, browserDumpLocalOwnsLink);
      assert.strictEqual(browserDumpLocalOwnsLink.source, browserDumpLocal);
      assert.strictEqual(browserDumpLocalOwnsLink.target, globalDumpShared);
      assert.strictEqual(browserDumpLocalOwnsLink.importance, 0);

      // Renderer memory allocator dump.
      assert.lengthOf(rendererDump.memoryAllocatorDumps, 1);

      const rendererDumpLocal = rendererDump.getMemoryAllocatorDumpByFullName(
          'local');
      assert.isDefined(rendererDumpLocal);
      assert.include(rendererDump.memoryAllocatorDumps, rendererDumpLocal);
      checkDumpNumericsAndDiagnostics(rendererDumpLocal, {
        'area': new Scalar(unitlessNumber_smallerIsBetter, 9),
        'length': 3
      }, { 'color': 'blue' });
      assert.lengthOf(rendererDumpLocal.children, 0);
      assert.isUndefined(rendererDumpLocal.parent);

      assert.isDefined(rendererDumpLocal.owns);
      assert.lengthOf(rendererDumpLocal.ownedBy, 0);
      assert.lengthOf(rendererDumpLocal.retains, 0);
      assert.lengthOf(rendererDumpLocal.retainedBy, 1);

      const rendererDumpLocalOwnsLink = rendererDumpLocal.owns;
      assert.include(globalDumpShared.ownedBy, rendererDumpLocalOwnsLink);
      assert.strictEqual(rendererDumpLocalOwnsLink.source, rendererDumpLocal);
      assert.strictEqual(rendererDumpLocalOwnsLink.target, globalDumpShared);
      assert.strictEqual(rendererDumpLocalOwnsLink.importance, 1);

      // GPU memory allocator dumps.
      assert.lengthOf(gpuDump.memoryAllocatorDumps, 2);

      const gpuDumpLocal1 = gpuDump.getMemoryAllocatorDumpByFullName('local1');
      assert.isDefined(gpuDumpLocal1);
      assert.include(gpuDump.memoryAllocatorDumps, gpuDumpLocal1);
      checkDumpNumericsAndDiagnostics(gpuDumpLocal1, {
        'area': new Scalar(unitlessNumber_smallerIsBetter, 9)
      }, { 'state': 'ON', 'color': 'blue' });
      assert.lengthOf(gpuDumpLocal1.children, 0);
      assert.isUndefined(gpuDumpLocal1.parent);

      assert.isDefined(gpuDumpLocal1.owns);
      assert.lengthOf(gpuDumpLocal1.ownedBy, 1);
      assert.lengthOf(gpuDumpLocal1.retains, 1);
      assert.lengthOf(gpuDumpLocal1.retainedBy, 0);

      const gpuDumpLocal1OwnsLink = gpuDumpLocal1.owns;
      assert.include(globalDumpShared.ownedBy, gpuDumpLocal1OwnsLink);
      assert.strictEqual(gpuDumpLocal1OwnsLink.source, gpuDumpLocal1);
      assert.strictEqual(gpuDumpLocal1OwnsLink.target, globalDumpShared);
      assert.strictEqual(gpuDumpLocal1OwnsLink.importance, -1);

      const gpuDumpLocal1RetainsLink = gpuDumpLocal1.retains[0];
      assert.include(rendererDumpLocal.retainedBy, gpuDumpLocal1RetainsLink);
      assert.strictEqual(gpuDumpLocal1RetainsLink.source, gpuDumpLocal1);
      assert.strictEqual(gpuDumpLocal1RetainsLink.target, rendererDumpLocal);
      assert.isUndefined(gpuDumpLocal1RetainsLink.importance);

      const gpuDumpLocal2 = gpuDump.getMemoryAllocatorDumpByFullName('local2');
      assert.isDefined(gpuDumpLocal2);
      assert.include(gpuDump.memoryAllocatorDumps, gpuDumpLocal2);
      checkDumpNumericsAndDiagnostics(gpuDumpLocal2, {
        'temperature': new Scalar(unitlessNumber_smallerIsBetter, 100)
      }, {});
      assert.lengthOf(gpuDumpLocal2.children, 0);
      assert.isUndefined(gpuDumpLocal2.parent);

      assert.isDefined(gpuDumpLocal2.owns);
      assert.lengthOf(gpuDumpLocal2.ownedBy, 0);
      assert.lengthOf(gpuDumpLocal2.retains, 0);
      assert.lengthOf(gpuDumpLocal2.retainedBy, 0);

      const gpuDumpLocal2OwnsLink = gpuDumpLocal2.owns;
      assert.include(gpuDumpLocal1.ownedBy, gpuDumpLocal2OwnsLink);
      assert.strictEqual(gpuDumpLocal2OwnsLink.source, gpuDumpLocal2);
      assert.strictEqual(gpuDumpLocal2OwnsLink.target, gpuDumpLocal1);
      assert.strictEqual(gpuDumpLocal2OwnsLink.importance, 1);
    }

    const events = [
      {
        name: 'browser',
        pid: 42,
        ts: 10,
        cat: 'test',
        tid: 53,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '100'
            },
            allocators: {
              'local': {
                guid: '3',
                attrs: {
                  mood: {type: 'string', units: '', value: 'very good'}
                }
              },
              'global/shared': {
                guid: '7',
                attrs: {
                  color: {type: 'string', units: '', value: 'blue'}
                }
              }
            },
            allocators_graph: [
              {
                source: '3',
                target: '7',
                type: 'ownership',
                importance: 0
              }
            ]
          }
        }
      },
      {
        name: 'renderer',
        pid: 43,
        ts: 11,
        cat: 'test',
        tid: 53,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '200'
            },
            allocators: {
              'local': {
                guid: '4',
                attrs: {
                  length: {type: 'scalar', units: 'bytes', value: '3'}
                }
              },
              'global/shared': {
                guid: '7',
                attrs: {
                  area: {type: 'scalar', units: 'sq ft', value: '9'}
                }
              }
            },
            allocators_graph: [
              {
                source: '4',
                target: '7',
                type: 'ownership',
                importance: 1
              }
            ]
          }
        }
      },
      {
        name: 'gpu',
        pid: 44,
        ts: 10.5,
        cat: 'test',
        tid: 53,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '300'
            },
            allocators: {
              'local1': {
                guid: '5',
                attrs: {
                  state: {type: 'string', units: '', value: 'ON'}
                }
              },
              'local2': {
                guid: '6',
                attrs: {
                  temperature: {type: 'scalar', units: 'C', value: '64'}
                }
              }
            },
            allocators_graph: [
              {
                source: '5',
                target: '7',
                type: 'ownership',
                importance: -1
              },
              {
                source: '6',
                target: '5',
                type: 'ownership',
                importance: 1
              },
              {
                source: '5',
                target: '4',
                type: 'retention'
              }
            ]
          }
        }
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importMemoryDumps_memoryAllocatorDumpsMissingFields', function() {
    function checkModel(m) {
      const p = m.getProcess(42);
      const d = p.memoryDumps[0];

      assert.strictEqual(d.memoryAllocatorDumps.length, 1);
      const noCrashRoot = d.getMemoryAllocatorDumpByFullName('no_crash');
      assert.lengthOf(noCrashRoot.children, 0);
      checkDumpNumericsAndDiagnostics(noCrashRoot, {}, {});
      assert.isUndefined(noCrashRoot.parent);
      assert.isUndefined(noCrashRoot.guid);
    }

    const events = [
      {
        name: 'a',
        pid: 42,
        ts: 10,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            allocators: {
              'no_crash': {
                /* Missing GUID and attributes. */
              }
            }
          }
        }
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importMemoryDumps_weakMemoryAllocatorDumps', function() {
    function checkModel(m) {
      const p = m.getProcess(42);
      const d = p.memoryDumps[0];
      const memoryAllocatorDumps = d.memoryAllocatorDumps;
      assert.lengthOf(memoryAllocatorDumps, 6);

      function checkDump(dump, expectedFullName, expectedGuid, expectedParent,
          expectedChildCount, expectedOwnsLink, expectedOwnedByLinkCount) {
        assert.strictEqual(dump.fullName, expectedFullName);
        assert.strictEqual(dump.guid, expectedGuid);
        assert.strictEqual(dump.parent, expectedParent);
        assert.lengthOf(dump.children, expectedChildCount);
        assert.strictEqual(dump.owns, expectedOwnsLink);
        assert.lengthOf(dump.ownedBy, expectedOwnedByLinkCount);
        assert.strictEqual(
            d.getMemoryAllocatorDumpByFullName(expectedFullName), dump);
      }

      function checkOwnsLink(ownerDump, expectedTarget) {
        assert.strictEqual(ownerDump.owns.source, ownerDump);
        assert.strictEqual(ownerDump.owns.target, expectedTarget);
      }

      // Check root_sink/* dumps.
      const rootSink = d.memoryAllocatorDumps[3];
      checkDump(rootSink, 'root_sink', '100', undefined, 1, undefined, 2);
      const childSink = rootSink.children[0];
      checkDump(childSink, 'root_sink/child_sink', '200', rootSink, 1,
          undefined, 1);
      const descendantSink = childSink.children[0];
      checkDump(descendantSink, 'root_sink/child_sink/descendant_sink', '300',
          childSink, 0, undefined, 2);

      // Check strong_root/* dumps.
      const strongRoot = d.memoryAllocatorDumps[4];
      checkDump(strongRoot, 'strong_root', '4', undefined, 0,
          rootSink.ownedBy[0], 0);

      // Check inferred_strong_root/* dumps.
      const inferredStrongRoot = d.memoryAllocatorDumps[0];
      checkDump(inferredStrongRoot, 'inferred_strong_root', undefined,
          undefined, 1, undefined, 0);
      const child2Strong = inferredStrongRoot.children[0];
      checkDump(child2Strong, 'inferred_strong_root/child2_strong', '9',
          inferredStrongRoot, 0, childSink.ownedBy[0], 0);

      // Check inferred_strong_root2/* dumps.
      const inferredStrongRoot2 = d.memoryAllocatorDumps[1];
      checkDump(inferredStrongRoot2, 'inferred_strong_root2', undefined,
          undefined, 1, undefined, 0);
      const inferredStrongChild = inferredStrongRoot2.children[0];
      checkDump(inferredStrongChild,
          'inferred_strong_root2/inferred_strong_child', undefined,
          inferredStrongRoot2, 2, undefined, 0);
      const desc1Strong = inferredStrongChild.children[0];
      checkDump(desc1Strong,
          'inferred_strong_root2/inferred_strong_child/desc1_strong', '11',
          inferredStrongChild, 0, descendantSink.ownedBy[0], 0);
      const desc3Strong = inferredStrongChild.children[1];
      checkDump(desc3Strong,
          'inferred_strong_root2/inferred_strong_child/desc3_strong', '13',
          inferredStrongChild, 0, descendantSink.ownedBy[1], 0);

      // Check strong_root2/* dumps.
      const strongRoot2 = d.memoryAllocatorDumps[5];
      checkDump(strongRoot2, 'strong_root2', '15', undefined, 0,
          rootSink.ownedBy[1], 0);

      // Check inferred_strong_root3/* dumps.
      const inferredStrongRoot3 = d.memoryAllocatorDumps[2];
      checkDump(inferredStrongRoot3, 'inferred_strong_root3', undefined,
          undefined, 0, undefined, 0);

      // Check the links.
      checkOwnsLink(strongRoot, rootSink);
      checkOwnsLink(child2Strong, childSink);
      checkOwnsLink(desc1Strong, descendantSink);
      checkOwnsLink(desc3Strong, descendantSink);
      checkOwnsLink(strongRoot2, rootSink);

      // Check that the removed weak dumps are not indexed.
      [
        'weak_root',
        'weak_root/removed_child',
        'weak_root/inferred_removed_child',
        'weak_root/inferred_removed_child/removed_descendant',
        'strong_root/weak_child',
        'strong_root/inferred_weak_child/weak_descendant',
        'inferred_weak_root',
        'inferred_weak_root/inferred_weak_child',
        'inferred_weak_root/inferred_weak_child/weak_descendant',
        'inferred_strong_root/child1_weak',
        'inferred_strong_root/child3_weak',
        'inferred_strong_root2/inferred_strong_child/desc2_weak',
        'inferred_strong_root2/weak_child',
        'strong_root2/weak_child',
        'strong_root2/removed_descendant',
        'removed_root',
        'removed_root/removed_child',
        'inferred_strong_root3/removed_child'
      ].forEach(function(fullName) {
        assert.isUndefined(d.getMemoryAllocatorDumpByFullName(fullName));
      });
    }

    const events = [
      {
        pid: 42,
        ts: 10,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            allocators: {
              // Sinks for ownership edges (to check that the correct ownership
              // edges are removed).
              'root_sink': { guid: '100', attrs: {} },
              'root_sink/child_sink': { guid: '200', attrs: {} },
              'root_sink/child_sink/descendant_sink': {
                guid: '300', attrs: {}
              },

              // Note: 'removed' in the name of a dump means that the dump will
              // be removed despite being non-weak (strong), e.g. due to one of
              // its ancestors being weak.

              // All descendants of a weak root dump should be removed.
              'weak_root': { guid: '1', attrs: {}, flags: 1 },
              'weak_root/removed_child': { guid: '2', attrs: {} },
              'weak_root/inferred_removed_child/removed_descendant': {
                guid: '3', attrs: {}, flags: 0
              },

              // A strong root should be kept even if all its descendants are
              // weak.
              'strong_root': { guid: '4', attrs: {}, flags: 0 },
              'strong_root/weak_child': { guid: '5', attrs: {}, flags: 1 },
              'strong_root/inferred_weak_child/weak_descendant': {
                guid: '6', attrs: {}, flags: 1
              },

              // All inferred ancestors of a weak descendant should be marked
              // weak and, consequently, removed (provided that they don't have
              // any non-weak descendants).
              'inferred_weak_root/inferred_weak_child/weak_descendant': {
                guid: '7', attrs: {}, flags: 1
              },

              // An inferred dump should be marked non-weak if it has at least
              // one strong descendant.
              'inferred_strong_root/child1_weak': {
                guid: '8', attrs: {}, flags: 1
              },
              'inferred_strong_root/child2_strong': {
                guid: '9', attrs: {}
              },
              'inferred_strong_root/child3_weak': {
                guid: '10', attrs: {}, flags: 1
              },
              'inferred_strong_root2/inferred_strong_child/desc1_strong': {
                guid: '11', attrs: {}
              },
              'inferred_strong_root2/inferred_strong_child/desc2_weak': {
                guid: '12', attrs: {}, flags: 1
              },
              'inferred_strong_root2/inferred_strong_child/desc3_strong': {
                guid: '13', attrs: {}
              },
              'inferred_strong_root2/weak_child': {
                guid: '14', attrs: {}, flags: 1
              },

              // A desdendant dump should be removed if it has a weak ancestor.
              'strong_root2': { guid: '15', attrs: {} },
              'strong_root2/weak_child': { guid: '16', attrs: {}, flags: 1 },
              'strong_root2/weak_child/removed_descendant': {
                guid: '17', attrs: {}
              },

              // Check that "weakness" also propagates across ownership edges.
              'removed_root': { guid: '18', attrs: {} },
              'removed_root/removed_child': {
                guid: '19', attrs: {}
              },
              'inferred_strong_root3/removed_child': {
                guid: '20', attrs: {}
              },
            },
            allocators_graph: [
              { source: '1', target: '100', type: 'ownership' },
              { source: '2', target: '200', type: 'ownership' },
              { source: '3', target: '300', type: 'ownership' },

              { source: '4', target: '100', type: 'ownership' },  // Kept.
              { source: '5', target: '200', type: 'ownership' },
              { source: '6', target: '300', type: 'ownership' },

              { source: '7', target: '300', type: 'ownership' },

              { source: '8', target: '200', type: 'ownership' },
              { source: '9', target: '200', type: 'ownership' },  // Kept.
              { source: '10', target: '200', type: 'ownership' },
              { source: '11', target: '300', type: 'ownership' },  // Kept.
              { source: '12', target: '300', type: 'ownership' },
              { source: '13', target: '300', type: 'ownership' },  // Kept.
              { source: '14', target: '200', type: 'ownership' },

              { source: '15', target: '100', type: 'ownership' },  // Kept.
              { source: '16', target: '200', type: 'ownership' },
              { source: '17', target: '300', type: 'ownership' },

              { source: '18', target: '3' /* not a sink */, type: 'ownership' },
              { source: '19', target: '200', type: 'ownership' },
              { source: '20', target: '19' /* not a sink */, type: 'ownership' }
            ]
          }
        }
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importMemoryDumps_levelsOfDetail', function() {
    function checkLevelsOfDetail(pmdSpecifications, expectedGlobalLevelOfDetail,
        expectedProcessLevelsOfDetail, expectedHasWarnings) {
      function checkModel(model) {
        // Check GlobalMemoryDump level of detail.
        assert.lengthOf(model.globalMemoryDumps, 1);
        assert.strictEqual(model.globalMemoryDumps[0].levelOfDetail,
            expectedGlobalLevelOfDetail);

        // Check ProcessMemoryDumps levels of detail.
        assert.lengthOf(Object.keys(model.processes),
            expectedProcessLevelsOfDetail.length);
        for (let i = 0; i < expectedProcessLevelsOfDetail.length; i++) {
          const process = model.getProcess(i);
          assert.lengthOf(process.memoryDumps, 1);
          assert.strictEqual(process.memoryDumps[0].levelOfDetail,
              expectedProcessLevelsOfDetail[i]);
        }

        assert.strictEqual(model.hasImportWarnings, expectedHasWarnings);
      }

      const events = [];
      pmdSpecifications.forEach(function(singlePmdSpecifications, pid) {
        singlePmdSpecifications.forEach(function(singlePmdSpecification) {
          const dumps = {};
          if (singlePmdSpecification.levelOfDetail !== undefined) {
            dumps.level_of_detail = singlePmdSpecification.levelOfDetail;
          }
          if (singlePmdSpecification.vmRegions) {
            dumps.process_mmaps = {
              vm_regions: [
                { sa: 'f0', sz: '150', pf: 6, mf: '[stack]', bs: { pss: 'ff'} }
              ]
            };
          }
          events.push({
            name: 'process_' + pid,
            pid,
            ts: 10,
            ph: 'v',
            id: '0x0001',
            cat: 'memory-infra',
            args: {
              dumps
            }
          });
        });
      });
      checkParsedAndStreamInput(events, checkModel);
    }

    // Legacy trace events (without levels of detail).
    checkLevelsOfDetail([[{}]], LIGHT, [LIGHT], false);
    checkLevelsOfDetail([[{ vmRegions: true }]], DETAILED, [DETAILED], false);
    checkLevelsOfDetail(
        [
          [{}] /* raw composable PMD1 events */,
          [{}, {}] /* raw composable PMD2 events */
        ],
        LIGHT /* expected GMD level of detail */,
        [LIGHT, LIGHT] /* expected PMD levels of detail */,
        false /* no warnings expected */);
    checkLevelsOfDetail(
        [
          [{}, { vmRegions: true }, {}],
          [{ vmRegions: true }, {}]
        ],
        DETAILED, [DETAILED, DETAILED], false);

    // Well-formed traces events (VM regions should be irrelevant).
    checkLevelsOfDetail(
        [
          [{ levelOfDetail: 'light'}],
          [{ levelOfDetail: 'light', vmRegions: true }]
        ],
        LIGHT, [LIGHT, LIGHT], false);
    checkLevelsOfDetail(
        [
          [
            { levelOfDetail: 'detailed' }, { levelOfDetail: 'detailed' }
          ],
          [
            { levelOfDetail: 'detailed', vmRegions: true }
          ],
          [
            { levelOfDetail: 'detailed' },
            { levelOfDetail: 'detailed', vmRegions: true },
            { levelOfDetail: 'detailed' }
          ]
        ],
        DETAILED, [DETAILED, DETAILED, DETAILED], false);

    // Not so well-formed trace events.
    checkLevelsOfDetail(
        [
          [{}, { levelOfDetail: 'detailed'}, {}]
        ],
        DETAILED, [DETAILED], true);
    checkLevelsOfDetail(
        [
          [{ levelOfDetail: 'light' }],
          [{}],
          [{ levelOfDetail: 'detailed' }],
          [{ levelOfDetail: 'light' }]],
        DETAILED, [LIGHT, LIGHT, DETAILED, LIGHT], true);
    checkLevelsOfDetail(
        [
          [{ levelOfDetail: 'light' }, { levelOfDetail: 'detailed' }],
          [{}],
          [{ levelOfDetail: 'light' }, {}]],
        DETAILED, [DETAILED, LIGHT, LIGHT], true);
    checkLevelsOfDetail(
        [
          [{ levelOfDetail: 'invalid' }, { levelOfDetail: 'light' }],
          [{ levelOfDetail: 'invalid' }]],
        LIGHT, [LIGHT, LIGHT], true);
  });

  test('importMemoryDumps_heapDumps_oldFormat', function() {
    function checkModel(m) {
      const p1 = m.getProcess(21);
      const p2 = m.getProcess(42);
      assert.lengthOf(m.globalMemoryDumps, 2);
      assert.lengthOf(p1.memoryDumps, 2);
      assert.lengthOf(p2.memoryDumps, 1);

      // Stack frames.
      const frameIdToTitle = {};
      for (const [id, f] of Object.entries(m.stackFrames)) {
        frameIdToTitle[id] = f.title;
      }
      assert.deepEqual(
          frameIdToTitle,
          {
            'p21:0': 'FrameView::layout',
            'p21:0:self': '<self>',
            'p21:1': 'MessageLoop::RunTask',
            'p42::self': '<self>',
            'p42:0': 'MessageLoop::RunTask',
            'p42:0:self': '<self>',
            'p42:1': 'TimerBase::run',
            'p42:TWO': 'ScheduledAction::execute',
            'p42:3': 'FunctionCall',
            'p42:3:self': '<self>',
            'p42:4': 'UpdateLayoutTree',
            'p42:4:self': '<self>',
            'p42:5': 'MessageLoop::JogTask',
            'p42:5:self': '<self>'
          });

      // 1. Process 21, first dump.
      const pmd1 = p1.memoryDumps[0];
      const hds1 = pmd1.heapDumps;
      assert.sameMembers(Object.keys(hds1), ['partition_alloc']);

      const partitionAllocDump1 = hds1.partition_alloc;
      assert.strictEqual(partitionAllocDump1.processMemoryDump, pmd1);
      assert.strictEqual(partitionAllocDump1.allocatorName, 'partition_alloc');
      const partitionAllocEntries1 = partitionAllocDump1.entries;
      assert.lengthOf(partitionAllocEntries1, 2);
      checkHeapEntry(partitionAllocEntries1[0], partitionAllocDump1, 4096,
          undefined /* root */, undefined /* sum over all types */);
      checkHeapEntry(partitionAllocEntries1[1], partitionAllocDump1, 2748,
          ['<self>', 'FrameView::layout', 'MessageLoop::RunTask']);

      // 2. Process 21, second dump.
      const pmd2 = p1.memoryDumps[1];
      const hds2 = pmd2.heapDumps;
      assert.sameMembers(Object.keys(hds2), ['partition_alloc']);

      const partitionAllocDump2 = hds2.partition_alloc;
      assert.strictEqual(partitionAllocDump2.processMemoryDump, pmd2);
      assert.strictEqual(partitionAllocDump2.allocatorName, 'partition_alloc');
      const partitionAllocEntries2 = partitionAllocDump2.entries;
      assert.lengthOf(partitionAllocEntries2, 2);
      checkHeapEntry(partitionAllocEntries2[0], partitionAllocDump2, 8192,
          undefined /* root */, undefined /* sum over all types */);
      checkHeapEntry(partitionAllocEntries2[1], partitionAllocDump2, 3567,
          ['<self>', 'FrameView::layout', 'MessageLoop::RunTask'],
          undefined /* sum over all types */);

      // All heap dumps in Process 21 should use the same stack frames.
      assert.strictEqual(
          getFrame(partitionAllocEntries1[1], 0),
          getFrame(partitionAllocEntries2[1], 0));

      // 3. Process 42.
      const pmd3 = p2.memoryDumps[0];
      const hds3 = pmd3.heapDumps;
      assert.sameMembers(Object.keys(hds3), ['partition_alloc', 'malloc']);

      const partitionAllocDump3 = hds3.partition_alloc;
      assert.strictEqual(partitionAllocDump3.processMemoryDump, pmd3);
      assert.strictEqual(partitionAllocDump3.allocatorName, 'partition_alloc');
      const partitionAllocEntries3 = partitionAllocDump3.entries;
      assert.lengthOf(partitionAllocEntries3, 6);
      checkHeapEntry(partitionAllocEntries3[0], partitionAllocDump3, 3043272,
          undefined /* root */, 'blink::Event');
      checkHeapEntry(partitionAllocEntries3[1], partitionAllocDump3, 6086545,
          undefined /* root */, undefined /* sum over all types */);
      checkHeapEntry(partitionAllocEntries3[2], partitionAllocDump3, 1521636,
          undefined /* root */, 'blink::ContextLifecycleObserver*');
      checkHeapEntry(partitionAllocEntries3[3], partitionAllocDump3, 5991638,
          ['<self>'], undefined /* sum over all types */);
      checkHeapEntry(partitionAllocEntries3[4], partitionAllocDump3, 6384,
          ['<self>', 'UpdateLayoutTree', 'TimerBase::run',
            'MessageLoop::RunTask'], undefined /* sum over all types */);
      checkHeapEntry(partitionAllocEntries3[5], partitionAllocDump3, 58280,
          ['<self>', 'FunctionCall', 'ScheduledAction::execute',
            'TimerBase::run', 'MessageLoop::RunTask'],
          undefined /* sum over all types */);

      const mallocDump3 = hds3.malloc;
      assert.strictEqual(mallocDump3.processMemoryDump, pmd3);
      assert.strictEqual(mallocDump3.allocatorName, 'malloc');
      const mallocEntries3 = mallocDump3.entries;
      assert.lengthOf(mallocEntries3, 4);
      checkHeapEntry(mallocEntries3[0], mallocDump3, 1929, undefined /* root */,
          undefined /* sum over all types */);
      checkHeapEntry(mallocEntries3[1], mallocDump3, 291,
          ['<self>', 'MessageLoop::RunTask'],
          undefined /* sum over all types */);
      checkHeapEntry(mallocEntries3[2], mallocDump3, 1110,
          ['<self>', 'MessageLoop::JogTask'],
          undefined /* sum over all types */);
      checkHeapEntry(mallocEntries3[3], mallocDump3, 205, undefined /* root */,
          'blink::ContextLifecycleObserver*');

      // All heap dumps in Process 42 should use the same stack frames.
      assert.strictEqual(
          getFrame(partitionAllocEntries3[5], 3),
          getFrame(partitionAllocEntries3[4], 2));
      assert.strictEqual(
          getFrame(mallocEntries3[1], 1),
          getFrame(partitionAllocEntries3[4], 3));
    }

    const events = [  // Intentionally shuffled.
      {
        pid: 21,
        ts: 9,
        ph: 'v',
        id: '0123',
        cat: 'memory-infra',
        args: {
          dumps: {
            heaps: {
              partition_alloc: {
                entries: [
                  { size: '1000' },
                  { bt: '0', size: 'abc' }
                ]
              }
            }
          }
        }
      },
      {
        pid: 42,
        ph: 'M',
        name: 'stackFrames',
        cat: 'memory-infra',
        args: {
          stackFrames: {
            '0': { name: 'MessageLoop::RunTask' },
            '1': { name: 'TimerBase::run', parent: '0' },
            'TWO': { name: 'ScheduledAction::execute', parent: '1' },
            '3': { name: 'FunctionCall', parent: 'TWO' },
            '4': { name: 'UpdateLayoutTree', parent: '1' },
            '5': { name: 'MessageLoop::JogTask' }
          }
        }
      },
      {
        pid: 42,
        ph: 'M',
        name: 'typeNames',
        cat: 'memory-infra',
        args: {
          typeNames: {
            // GCC.
            '22': '[unknown]',
            '23': 'testing::ManuallyAnnotatedMockClass',
            '24': 'const char* WTF::getStringWithTypeName() [with T = ' +
                'blink::Event]',
            '25': 'blink::ContextLifecycleObserver*',
            '26': 'const char* WTF::getStringWithTypeName() [with T = ' +
                'blink::WebFrame*]'
          }
        }
      },
      {
        pid: 42,
        ts: 10,
        ph: 'v',
        id: '0123',
        cat: 'memory-infra',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '0'
            },
            heaps: {
              partition_alloc: {
                entries: [
                  { type: '24', size: '2e6fc8' },
                  { size: '5cdf91' },
                  { type: '25', size: '1737e4' },
                  { bt: '', size: '5b6cd6' },
                  { bt: '4', size: '18f0' },
                  { bt: '3', size: 'e3a8' }
                ]
              },
              malloc: {
                entries: [
                  { size: '789' },
                  { bt: '0', size: '123' },
                  { bt: '5', size: '456' },
                  { type: '25', size: 'cd' }
                ]
              }
            }
          }
        }
      },
      {
        pid: 21,
        ph: 'M',
        name: 'stackFrames',
        cat: 'memory-infra',
        args: {
          stackFrames: {
            // Intentionally in reverse order.
            '0': { name: 'FrameView::layout', parent: '1' },
            '1': { name: 'MessageLoop::RunTask' }
          }
        }
      },
      {
        pid: 21,
        ts: 12,
        ph: 'v',
        id: '0987',
        cat: 'memory-infra',
        args: {
          dumps: {
            heaps: {
              partition_alloc: {
                entries: [
                  { size: '2000' },
                  { bt: '0', size: 'def' }
                ]
              }
            }
          }
        }
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('importMemoryDumps_heapDumps_newFormat', function() {
    function checkModel(m) {
      const p1 = m.getProcess(21);
      const p2 = m.getProcess(42);
      const p3 = m.getProcess(63);
      const p4 = m.getProcess(84);
      assert.lengthOf(m.globalMemoryDumps, 2);
      assert.lengthOf(p1.memoryDumps, 2);
      assert.lengthOf(p2.memoryDumps, 1);
      assert.lengthOf(p3.memoryDumps, 1);
      assert.lengthOf(p4.memoryDumps, 1);

      // Stack frames.
      const frameIdToTitle = {};
      for (const [id, f] of Object.entries(m.stackFrames)) {
        frameIdToTitle[id] = f.title;
      }
      assert.deepEqual(
          frameIdToTitle,
          {
            'p21:0': 'FrameView::layout',
            'p21:A': '<self>',
            'p21:1': 'MessageLoop::RunTask',
            'p42:-1': '<self>',
            'p42:0': 'MessageLoop::RunTask',
            'p42:0.5': '<self>',
            'p42:1': 'TimerBase::run',
            'p42:TWO': 'ScheduledAction::execute',
            'p42:2.72': '<self>',
            'p42:3': 'FunctionCall',
            'p42:\u03C0': '<self>',
            'p42:4': 'UpdateLayoutTree',
            'p42:FOUR-AND-A-BIT': '<self>',
            'p42:5': 'MessageLoop::JogTask',
            'p42:NaN': '<self>',
            'p84:5': 'MessageLoop::WalkTask'
          });

      // 1. Process 21, first dump.
      const pmd1 = p1.memoryDumps[0];
      const hds1 = pmd1.heapDumps;
      assert.sameMembers(Object.keys(hds1), ['partition_alloc']);

      const partitionAllocDump1 = hds1.partition_alloc;
      assert.strictEqual(partitionAllocDump1.processMemoryDump, pmd1);
      assert.strictEqual(partitionAllocDump1.allocatorName, 'partition_alloc');
      const partitionAllocEntries1 = partitionAllocDump1.entries;
      assert.lengthOf(partitionAllocEntries1, 2);
      checkHeapEntry(partitionAllocEntries1[0], partitionAllocDump1, 4096,
          undefined /* root */,
          'class v8::FunctionCallbackInfo<class v8::Value>');
      checkHeapEntry(partitionAllocEntries1[1], partitionAllocDump1, 2748,
          ['<self>', 'FrameView::layout', 'MessageLoop::RunTask'],
          undefined /* sum over all types */);

      // 2. Process 21, second dump.
      const pmd2 = p1.memoryDumps[1];
      const hds2 = pmd2.heapDumps;
      assert.sameMembers(Object.keys(hds2), ['partition_alloc']);

      const partitionAllocDump2 = hds2.partition_alloc;
      assert.strictEqual(partitionAllocDump2.processMemoryDump, pmd2);
      assert.strictEqual(partitionAllocDump2.allocatorName, 'partition_alloc');
      const partitionAllocEntries2 = partitionAllocDump2.entries;
      assert.lengthOf(partitionAllocEntries2, 3);
      checkHeapEntry(partitionAllocEntries2[0], partitionAllocDump2, 8192,
          undefined /* root */, undefined /* sum over all types */);
      checkHeapEntry(partitionAllocEntries2[1], partitionAllocDump2, 3567,
          ['<self>', 'FrameView::layout', 'MessageLoop::RunTask'],
          'class v8::FunctionCallbackInfo<class v8::Value>');
      checkHeapEntry(partitionAllocEntries2[2], partitionAllocDump2, 4095,
          ['FrameView::layout', 'MessageLoop::RunTask'],
          undefined /* sum over all types */);

      // All heap dumps in Process 21 should use the same stack frames.
      assert.strictEqual(
          getFrame(partitionAllocEntries1[1], 0),
          getFrame(partitionAllocEntries2[1], 0));
      assert.strictEqual(
          getFrame(partitionAllocEntries2[2], 0),
          getFrame(partitionAllocEntries2[1], 1));

      // 3. Process 42.
      const pmd3 = p2.memoryDumps[0];
      const hds3 = pmd3.heapDumps;
      assert.sameMembers(Object.keys(hds3), ['partition_alloc', 'malloc']);

      const partitionAllocDump3 = hds3.partition_alloc;
      assert.strictEqual(partitionAllocDump3.processMemoryDump, pmd3);
      assert.strictEqual(partitionAllocDump3.allocatorName, 'partition_alloc');
      const partitionAllocEntries3 = partitionAllocDump3.entries;
      assert.lengthOf(partitionAllocEntries3, 7);
      checkHeapEntry(partitionAllocEntries3[0], partitionAllocDump3, 6086545,
          undefined /* root */, undefined /* sum over all types */);
      checkHeapEntry(partitionAllocEntries3[1], partitionAllocDump3, 3043272,
          undefined /* root */, 'blink::Event');
      checkHeapEntry(partitionAllocEntries3[2], partitionAllocDump3, 1521636,
          undefined /* root */, 'blink::ContextLifecycleObserver *');
      checkHeapEntry(partitionAllocEntries3[3], partitionAllocDump3, 5991638,
          ['<self>'], '[unknown]');
      checkHeapEntry(partitionAllocEntries3[4], partitionAllocDump3, 6384,
          ['<self>', 'UpdateLayoutTree', 'TimerBase::run',
            'MessageLoop::RunTask'], undefined /* sum over all types */, 256);
      checkHeapEntry(partitionAllocEntries3[5], partitionAllocDump3, 3192,
          ['<self>', 'UpdateLayoutTree', 'TimerBase::run',
            'MessageLoop::RunTask'], 'blink::WebFrame *');
      checkHeapEntry(partitionAllocEntries3[6], partitionAllocDump3, 58280,
          ['<self>', 'FunctionCall', 'ScheduledAction::execute',
            'TimerBase::run', 'MessageLoop::RunTask'],
          undefined /* sum over all types */);

      const mallocDump3 = hds3.malloc;
      assert.strictEqual(mallocDump3.processMemoryDump, pmd3);
      assert.strictEqual(mallocDump3.allocatorName, 'malloc');
      const mallocEntries3 = mallocDump3.entries;
      assert.lengthOf(mallocEntries3, 4);
      checkHeapEntry(mallocEntries3[0], mallocDump3, 1929, undefined /* root */,
          undefined /* sum over all types */, 80);
      checkHeapEntry(mallocEntries3[1], mallocDump3, 291,
          ['<self>', 'MessageLoop::RunTask'],
          undefined /* sum over all types */, 96);
      checkHeapEntry(mallocEntries3[2], mallocDump3, 1110,
          ['<self>', 'MessageLoop::JogTask'],
          undefined /* sum over all types */, 112);
      checkHeapEntry(mallocEntries3[3], mallocDump3, 205,
          ['FunctionCall', 'ScheduledAction::execute', 'TimerBase::run',
            'MessageLoop::RunTask'], 'blink::ContextLifecycleObserver *', 128);

      // All heap dumps in Process 42 should use the same stack frames.
      assert.strictEqual(
          getFrame(partitionAllocEntries3[5], 0),
          getFrame(partitionAllocEntries3[4], 0));
      assert.strictEqual(
          getFrame(partitionAllocEntries3[6], 3),
          getFrame(partitionAllocEntries3[4], 2));
      assert.strictEqual(
          getFrame(mallocEntries3[1], 1),
          getFrame(partitionAllocEntries3[4], 3));
      assert.strictEqual(
          getFrame(mallocEntries3[3], 0),
          getFrame(partitionAllocEntries3[6], 1));

      // 4. Process 63.
      const pmd4 = p3.memoryDumps[0];
      const hds4 = pmd4.heapDumps;
      assert.sameMembers(Object.keys(hds4), ['winheap']);

      const winheapDump = hds4.winheap;
      assert.strictEqual(winheapDump.processMemoryDump, pmd4);
      assert.strictEqual(winheapDump.allocatorName, 'winheap');
      const winheapEntries = winheapDump.entries;
      assert.lengthOf(winheapEntries, 1);
      checkHeapEntry(winheapEntries[0], winheapDump, 65536,
          undefined /* root */, undefined /* sum over all types */);

      // 5. Process 84.
      const pmd5 = p4.memoryDumps[0];
      const hds5 = pmd5.heapDumps;
      assert.sameMembers(Object.keys(hds5), ['malloc']);

      const mallocDump4 = hds5.malloc;
      assert.strictEqual(mallocDump4.processMemoryDump, pmd5);
      assert.strictEqual(mallocDump4.allocatorName, 'malloc');
      const mallocEntries4 = mallocDump4.entries;
      assert.lengthOf(mallocEntries4, 1);
      checkHeapEntry(mallocEntries4[0], mallocDump4, 43981,
          ['MessageLoop::WalkTask'], 'content::Manually');
    }

    const events = [  // Intentionally shuffled.
      {
        pid: 21,
        ts: 9,
        ph: 'v',
        id: '0123',
        cat: 'memory-infra',
        args: {
          dumps: {
            heaps: {
              partition_alloc: {
                entries: [
                  { bt: '', type: '25', size: '1000' },
                  { bt: 'A', size: 'abc' }
                ]
              }
            }
          }
        }
      },
      {
        pid: 42,
        ph: 'M',
        name: 'stackFrames',
        cat: 'memory-infra',
        args: {
          stackFrames: {
            '-1': { name: '<self>' },
            '0': { name: 'MessageLoop::RunTask' },
            '0.5': { name: '<self>', parent: '0' },
            '1': { name: 'TimerBase::run', parent: '0' },
            'TWO': { name: 'ScheduledAction::execute', parent: '1' },
            '2.72': { name: '<self>', parent: 'TWO' },
            '3': { name: 'FunctionCall', parent: 'TWO' },
            '\u03C0': { name: '<self>', parent: '3' },
            '4': { name: 'UpdateLayoutTree', parent: '1' },
            'FOUR-AND-A-BIT': { name: '<self>', parent: '4' },
            '5': { name: 'MessageLoop::JogTask' },
            'NaN': { name: '<self>', parent: '5' }
          }
        }
      },
      {
        pid: 42,
        ph: 'M',
        name: 'typeNames',
        cat: 'memory-infra',
        args: {
          typeNames: {
            // Clang.
            '22': '[unknown]',
            '23': 'testing::ManuallyAnnotatedMockClass',
            '24': 'const char *WTF::getStringWithTypeName() [T = ' +
                'blink::Event]',
            '25': 'blink::ContextLifecycleObserver *',
            '26': 'const char *WTF::getStringWithTypeName() [T = ' +
                'blink::WebFrame *]'
          }
        }
      },
      {
        pid: 42,
        ts: 10,
        ph: 'v',
        id: '0123',
        cat: 'memory-infra',
        args: {
          dumps: {
            process_totals: {
              resident_set_bytes: '0'
            },
            heaps: {
              partition_alloc: {
                entries: [
                  { bt: '' /* root */, size: '5cdf91' },
                  { bt: '' /* root */, type: '24', size: '2e6fc8' },
                  { bt: '' /* root */, type: '25', size: '1737e4' },
                  { bt: '-1', type: '22', size: '5b6cd6' },
                  { bt: 'FOUR-AND-A-BIT', size: '18f0', count: '100' },
                  { bt: 'FOUR-AND-A-BIT', type: '26', size: 'c78' },
                  { bt: '\u03C0', size: 'e3a8' }
                ]
              },
              malloc: {
                entries: [
                  { bt: '', size: '789', count: '50' },
                  { bt: '0.5', size: '123', count: '60' },
                  { bt: 'NaN', size: '456', count: '70' },
                  { bt: '3', type: '25', size: 'cd', count: '80' }
                ]
              }
            }
          }
        }
      },
      {
        pid: 21,
        ph: 'M',
        name: 'stackFrames',
        cat: 'memory-infra',
        args: {
          stackFrames: {
            // Intentionally in reverse order.
            'A': { name: '<self>', parent: '0' },
            '0': { name: 'FrameView::layout', parent: '1' },
            '1': { name: 'MessageLoop::RunTask' }
          }
        }
      },
      {
        pid: 21,
        ts: 12,
        ph: 'v',
        id: '0987',
        cat: 'memory-infra',
        args: {
          dumps: {
            heaps: {
              winheap: {
                entries: []  // Intentionally empty.
              },
              partition_alloc: {
                entries: [
                  { bt: '', size: '2000' },
                  { bt: 'A', type: '25', size: 'def' },
                  { bt: '3' /* invalid */, size: 'aaa' },
                  { bt: 'A', type: '24' /* invalid */, size: 'bbb' },
                  { bt: '0', size: 'fff' }
                ]
              }
            }
          }
        }
      },
      {
        pid: 21,
        ph: 'M',
        name: 'typeNames',
        cat: 'memory-infra',
        args: {
          typeNames: {
            // Microsoft Visual C++.
            '25': 'const char *__cdecl WTF::getStringWithTypeName<class ' +
                'v8::FunctionCallbackInfo<class v8::Value>>(void)'
          }
        }
      },
      {
        pid: 63,
        ph: 'M',
        name: 'stackFrames',
        cat: 'memory-infra',
        args: {
          stackFrames: {}  // Intentionally empty.
        }
      },
      {
        pid: 63,
        ph: 'M',
        name: 'typeNames',
        cat: 'memory-infra',
        args: {
          typeNames: {}  // Intentionally empty.
        }
      },
      {
        pid: 63,
        ts: 13,
        ph: 'v',
        id: '0987',
        cat: 'memory-infra',
        args: {
          dumps: {
            heaps: {
              winheap: {
                entries: [
                  { bt: '', size: '10000' }
                ]
              }
            }
          }
        }
      },
      {
        pid: 84,
        ph: 'M',
        name: 'stackFrames',
        cat: 'memory-infra',
        args: {
          stackFrames: {
            '5': { name: 'MessageLoop::WalkTask' }
          }
        }
      },
      {
        pid: 84,
        ph: 'M',
        name: 'typeNames',
        cat: 'memory-infra',
        args: {
          typeNames: {
            '0': '[unknown]',
            '1': 'base::All',
            '3': 'content::Manually',
            '4': 'net::Annotated'
          }
        }
      },
      {
        pid: 84,
        ts: 14,
        ph: 'v',
        id: '0987',
        cat: 'memory-infra',
        args: {
          dumps: {
            heaps: {
              malloc: {
                entries: [
                  { bt: '5', type: '3', size: 'abcd' }
                ]
              }
            }
          }
        }
      }
    ];
    // TODO(chiniforooshan): checkParsedAndStreamInput uses JSON.stringify and
    // then stringToUint8Array for converting events to a trace stream. The
    // current implementation of stringToUint8Array is not Unicode compatible.
    const m = makeModel(events);
    checkModel(m);
  });

  test('importMemoryDumps_composableDumps', function() {
    function checkModel(m) {
      function checkDumpTime(dump, expectedStart, expectedDuration) {
        assert.closeTo(dump.start, expectedStart / 1000, 1e-5);
        assert.closeTo(dump.duration, expectedDuration / 1000, 1e-5);
      }

      function checkLinkCounts(allocatorDump, expectedHasOwns,
          expectedOwnedByCount, expectedRetainsCount, expectedRetainedByCount) {
        assert.strictEqual(allocatorDump.owns !== undefined, expectedHasOwns);
        assert.lengthOf(allocatorDump.ownedBy, expectedOwnedByCount);
        assert.lengthOf(allocatorDump.retains, expectedRetainsCount);
        assert.lengthOf(allocatorDump.retainedBy, expectedRetainedByCount);
      }

      // Check the overall structure of the model and memory dumps.
      assert.sameMembers(Object.keys(m.processes), ['42', '68']);
      assert.lengthOf(m.globalMemoryDumps, 2);
      const gmd1 = m.globalMemoryDumps[0];
      assert.strictEqual(gmd1.model, m);
      checkDumpTime(gmd1, 9999, 6);
      const gmd2 = m.globalMemoryDumps[1];
      assert.strictEqual(gmd2.model, m);
      checkDumpTime(gmd2, 10003, 0);

      const p1 = m.getProcess(42);
      assert.lengthOf(p1.memoryDumps, 2);
      const pmd1 = p1.memoryDumps[0];
      checkDumpTime(pmd1, 10000, 5);
      assert.strictEqual(pmd1.globalMemoryDump, gmd1);
      assert.strictEqual(pmd1.process, p1);
      const pmd2 = p1.memoryDumps[1];
      checkDumpTime(pmd2, 10003, 0);
      assert.strictEqual(pmd2.globalMemoryDump, gmd2);
      assert.strictEqual(pmd2.process, p1);

      const p2 = m.getProcess(68);
      assert.lengthOf(p2.memoryDumps, 1);
      const pmd3 = p2.memoryDumps[0];
      checkDumpTime(pmd3, 9999, 0);
      assert.strictEqual(pmd3.globalMemoryDump, gmd1);
      assert.strictEqual(pmd3.process, p2);

      assert.deepEqual(gmd1.processMemoryDumps, {42: pmd1, 68: pmd3});
      assert.deepEqual(gmd2.processMemoryDumps, {42: pmd2});

      // Check the composed dump.
      assert.strictEqual(pmd1.levelOfDetail, LIGHT);

      const totals = pmd1.totals;
      assert.strictEqual(totals.residentBytes, 256);
      assert.isUndefined(totals.peakResidentBytes);
      assert.isUndefined(totals.arePeakResidentBytesResettable);
      assert.deepEqual(totals.platformSpecific, {private_bytes: 128});

      const vmRegions = pmd1.vmRegions;
      checkVMRegions(vmRegions, [
        {
          mappedFile: '[stack:20310]',
          startAddress: 240,
          sizeInBytes: 336,
          protectionFlags: VMRegion.PROTECTION_FLAG_READ |
              VMRegion.PROTECTION_FLAG_WRITE,
          byteStats: {
            proportionalResident: 158
          }
        }
      ]);

      const memoryAllocatorDumps = pmd1.memoryAllocatorDumps;
      assert.lengthOf(memoryAllocatorDumps, 2);

      const local1Dump = pmd1.getMemoryAllocatorDumpByFullName('local1');
      assert.strictEqual(memoryAllocatorDumps[0], local1Dump);
      assert.strictEqual(local1Dump.fullName, 'local1');
      assert.isUndefined(local1Dump.parent);
      assert.lengthOf(local1Dump.children, 0);
      checkDumpNumericsAndDiagnostics(local1Dump, {},
          { 'A': 'blue', 'B': 'red' });
      checkLinkCounts(local1Dump, true /* owns */, 0 /* owned by */,
          0 /* retains */, 0 /* retained by */);

      const local2Dump = pmd1.getMemoryAllocatorDumpByFullName('local2');
      assert.strictEqual(memoryAllocatorDumps[1], local2Dump);
      assert.strictEqual(local2Dump.fullName, 'local2');
      assert.isUndefined(local2Dump.parent);
      assert.lengthOf(local2Dump.children, 0);
      checkDumpNumericsAndDiagnostics(local2Dump, {}, { 'B': 'yellow' });
      checkLinkCounts(local2Dump, false /* owns */, 0 /* owned by */,
          1 /* retains */, 0 /* retained by */);

      const heapDumps = pmd1.heapDumps;
      assert.sameMembers(Object.keys(heapDumps), ['partition_alloc']);
      const heapDump = heapDumps.partition_alloc;
      assert.strictEqual(heapDump.processMemoryDump, pmd1);
      assert.strictEqual(heapDump.allocatorName, 'partition_alloc');
      const entries = heapDump.entries;
      assert.lengthOf(entries, 1);
      assert.strictEqual(entries[0].heapDump, heapDump);
      assert.strictEqual(
          entries[0].leafStackFrame.title, 'MessageLoop::RunTask');
      assert.strictEqual(entries[0].objectTypeName, 'cc::SurfaceFactory');
      assert.strictEqual(entries[0].size, 1280);

      // Check the other dumps.
      assert.strictEqual(pmd2.levelOfDetail, DETAILED);
      assert.isUndefined(pmd2.vmRegions);
      assert.lengthOf(pmd2.memoryAllocatorDumps, 1);
      const otherLocal1Dump = pmd2.getMemoryAllocatorDumpByFullName('local1');
      assert.strictEqual(otherLocal1Dump, pmd2.memoryAllocatorDumps[0]);
      assert.strictEqual(otherLocal1Dump.fullName, 'local1');
      assert.isUndefined(otherLocal1Dump.parent);
      checkDumpNumericsAndDiagnostics(otherLocal1Dump, { 'A': 2989 }, {});
      assert.isUndefined(pmd2.heapDumps);
      checkLinkCounts(otherLocal1Dump, false /* owns */, 1 /* owned by */,
          0 /* retains */, 0 /* retained by */);

      assert.strictEqual(pmd3.levelOfDetail, DETAILED);
      const otherVmRegions = pmd3.vmRegions;
      checkVMRegions(otherVmRegions, [
        {
          mappedFile: '/dev/ashmem/dalvik',
          startAddress: 848,
          sizeInBytes: 592,
          protectionFlags: VMRegion.PROTECTION_FLAG_READ |
              VMRegion.PROTECTION_FLAG_EXECUTE,
          byteStats: {
            privateDirtyResident: 205
          }
        }
      ]);
      assert.lengthOf(pmd3.memoryAllocatorDumps, 0);
      assert.isUndefined(pmd3.heapDumps);

      // Check the global dumps.
      assert.lengthOf(gmd1.memoryAllocatorDumps, 2);
      const shared1Dump = gmd1.getMemoryAllocatorDumpByFullName('shared1');
      assert.strictEqual(shared1Dump, gmd1.memoryAllocatorDumps[0]);
      assert.strictEqual(shared1Dump.fullName, 'shared1');
      assert.isUndefined(shared1Dump.parent);
      checkDumpNumericsAndDiagnostics(shared1Dump, {},
          { 'A': 'purple', 'B': 'green' });
      checkLinkCounts(shared1Dump, false /* owns */, 1 /* owned by */,
          0 /* retains */, 0 /* retained by */);
      const shared2Dump = gmd1.getMemoryAllocatorDumpByFullName('shared2');
      assert.strictEqual(shared2Dump, gmd1.memoryAllocatorDumps[1]);
      assert.strictEqual(shared2Dump.fullName, 'shared2');
      assert.isUndefined(shared2Dump.parent);
      checkDumpNumericsAndDiagnostics(shared2Dump, {}, { 'A': 'cyan' });
      checkLinkCounts(shared2Dump, false /* owns */, 0 /* owned by */,
          0 /* retains */, 1 /* retained by */);

      assert.lengthOf(gmd2.memoryAllocatorDumps, 1);
      const otherShared1Dump = gmd2.getMemoryAllocatorDumpByFullName('shared1');
      assert.strictEqual(otherShared1Dump, gmd2.memoryAllocatorDumps[0]);
      assert.strictEqual(otherShared1Dump.fullName, 'shared1');
      assert.isUndefined(otherShared1Dump.parent);
      checkDumpNumericsAndDiagnostics(otherShared1Dump, {}, { 'A': 'brown' });
      checkLinkCounts(otherShared1Dump, true /* owns */, 0 /* owned by */,
          0 /* retains */, 0 /* retained by */);

      // Check the edges.
      const ownershipLink = local1Dump.owns;
      assert.strictEqual(shared1Dump.ownedBy[0], ownershipLink);
      assert.strictEqual(ownershipLink.source, local1Dump);
      assert.strictEqual(ownershipLink.target, shared1Dump);
      assert.strictEqual(ownershipLink.importance, 1);

      const retentionLink = local2Dump.retains[0];
      assert.strictEqual(shared2Dump.retainedBy[0], retentionLink);
      assert.strictEqual(retentionLink.source, local2Dump);
      assert.strictEqual(retentionLink.target, shared2Dump);
      assert.isUndefined(retentionLink.importance);

      const otherOwnershipLink = otherShared1Dump.owns;
      assert.strictEqual(otherLocal1Dump.ownedBy[0], otherOwnershipLink);
      assert.strictEqual(otherOwnershipLink.source, otherShared1Dump);
      assert.strictEqual(otherOwnershipLink.target, otherLocal1Dump);
      assert.isUndefined(otherOwnershipLink.importance);
    }

    // Split PMD1 over multiple PMD trace events, all of which should be merged
    // by the importer. Also inject some PMD trace events with different PIDs
    // or dump IDs, which should *not* be merged with the rest.
    const events = [
      {  // Heap dumps.
        pid: 42,
        ts: 10000,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            level_of_detail: 'light',
            heaps: {
              partition_alloc: {
                entries: [
                  { bt: '99', type: '888', size: '500' }
                ]
              }
            }
          }
        }
      },
      {  // PMD2 with a different dump id (should not be merged).
        pid: 42,
        ts: 10003,  // Same PID, so it will end up in the same process.
        ph: 'v',
        id: '0x0002',
        cat: 'memory-infra',
        args: {
          dumps: {
            level_of_detail: 'detailed',
            allocators: {
              'local1': {
                guid: '3',
                attrs: {
                  A: {type: 'scalar', units: 'bytes', value: '0xBAD'}
                }
              },
              'global/shared1': {
                guid: '4',
                attrs: {
                  A: {type: 'string', units: '', value: 'brown'}
                }
              }
            },
            allocators_graph: [
              {
                source: '4',
                target: '3',
                type: 'ownership'
              }
            ]
          }
        }
      },
      {  // Stack frames (required for heap dumps).
        pid: 42,
        ph: 'M',
        name: 'stackFrames',
        cat: 'memory-infra',
        args: {
          stackFrames: {
            '99': { name: 'MessageLoop::RunTask' }
          }
        }
      },
      {  // Allocator dumps.
        pid: 42,
        ts: 10001,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            level_of_detail: 'light',
            allocators: {
              'local1': {
                guid: '3',
                attrs: {
                  A: {type: 'string', units: '', value: 'blue'}
                }
              },
              'global/shared1': {
                guid: '7',
                attrs: {
                  A: {type: 'string', units: '', value: 'purple'}
                }
              },
              'global/shared2': {
                guid: '8',
                attrs: {
                  A: {type: 'string', units: '', value: 'cyan'}
                }
              }
            }
          }
        }
      },
      {  // PMD3 with a different PID (should not be merged).
        pid: 68,
        ts: 9999,
        ph: 'v',
        id: '0x0001',  // Same dump ID, so it will end up in the same GMD.
        cat: 'memory-infra',
        args: {
          dumps: {
            process_mmaps: {
              vm_regions: [
                {
                  sa: '350',
                  sz: '250',
                  pf: 5,
                  mf: '/dev/ashmem/dalvik',
                  bs: {
                    pd: 'cd'
                  }
                }
              ]
            }
          }
        }
      },
      {  // VM regions.
        pid: 42,
        ts: 10005,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            level_of_detail: 'light',
            process_mmaps: {
              vm_regions: [
                {
                  sa: 'f0',
                  sz: '150',
                  pf: 6,
                  mf: '[stack:20310]',
                  bs: {
                    pss: '9e'
                  }
                }
              ]
            }
          }
        }
      },
      {  // Totals.
        pid: 42,
        ts: 10003,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            level_of_detail: 'light',
            process_totals: {
              resident_set_bytes: '100',
              private_bytes: '80'  // OS-specific total.
            }
          }
        }
      },
      {  // Allocator dump edges.
        pid: 42,
        ts: 10004,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            level_of_detail: 'light',
            allocators_graph: [
              {
                source: '4',
                target: '8',
                type: 'retention'
              }
            ]
          }
        }
      },
      {  // Object type names (required for heap dumps).
        pid: 42,
        ph: 'M',
        name: 'typeNames',
        cat: 'memory-infra',
        args: {
          typeNames: {
            // GCC.
            '888': 'const char* WTF::getStringWithTypeName() [with T = ' +
                'cc::SurfaceFactory]'
          }
        }
      },
      {  // Allocator dumps and dump edges (should be merged).
        pid: 42,
        ts: 10002,
        ph: 'v',
        id: '0x0001',
        cat: 'memory-infra',
        args: {
          dumps: {
            level_of_detail: 'light',
            allocators: {
              'local1': {
                guid: '3',
                attrs: {
                  B: {type: 'string', units: '', value: 'red'}
                }
              },
              'local2': {
                guid: '4',
                attrs: {
                  B: {type: 'string', units: '', value: 'yellow'}
                }
              },
              'global/shared1': {
                guid: '7',
                attrs: {
                  B: {type: 'string', units: '', value: 'green'}
                }
              }
            },
            allocators_graph: [
              {
                source: '3',
                target: '7',
                type: 'ownership',
                importance: 1
              }
            ]
          }
        }
      }
    ];
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('importThreadInstantSliceWithStackFrame', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[2];
      assert.isDefined(t);
      assert.strictEqual(t.sliceGroup.slices.length, 1);

      const s0 = t.sliceGroup.slices[0];
      assert.strictEqual(s0.startStackFrame.title, 'frame7');
      assert.isUndefined(s0.endStackFrame);
    }

    const eventData = {
      traceEvents: [
        { name: 'a', args: {}, pid: 1, ts: 0, cat: 'baz', tid: 2, ph: 'I', s: 't', sf: 7 } // @suppress longLineCheck
      ],
      stackFrames: {
        '1': {
          category: 'm1',
          name: 'main'
        },
        '7': {
          category: 'm2',
          name: 'frame7',
          parent: '1'
        }
      }
    };
    checkParsedAndStreamInput(eventData, checkModel);
  });

  test('importDurationEventsWithStackFrames', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[2];
      assert.isDefined(t);
      assert.strictEqual(t.sliceGroup.slices.length, 1);

      const s0 = t.sliceGroup.slices[0];
      assert.strictEqual(s0.startStackFrame.title, 'frame7');
      assert.strictEqual(s0.endStackFrame.title, 'frame8');
    }

    const eventData = {
      traceEvents: [
        { name: 'a', args: {}, pid: 1, ts: 0, cat: 'baz', tid: 2, ph: 'B', sf: 7 }, // @suppress longLineCheck
        { name: 'b', args: {}, pid: 1, ts: 5, cat: 'baz', tid: 2, ph: 'E', sf: 8 } // @suppress longLineCheck
      ],
      stackFrames: {
        '1': {
          category: 'm1',
          name: 'main'
        },
        '7': {
          category: 'm2',
          name: 'frame7',
          parent: '1'
        },
        '8': {
          category: 'm2',
          name: 'frame8',
          parent: '1'
        }
      }
    };
    checkParsedAndStreamInput(eventData, checkModel);
  });

  test('annotationParsing', function() {
    const yComponents1 = [{stableId: '52.53', yPercentOffset: 0.5},
      {stableId: '52', yPercentOffset: 0.3}];
    const yComponents2 = [{stableId: '52.53', yPercentOffset: 0.7},
      {stableId: '52', yPercentOffset: 0.4}];
    const location1 = new tr.model.Location(0.1, yComponents1);
    const location2 = new tr.model.Location(0.2, yComponents2);

    function checkModel(m) {
      const annotations = m.getAllAnnotations();
      assert.strictEqual(annotations.length, 3);

      assert.isTrue(annotations[0] instanceof tr.model.XMarkerAnnotation);
      assert.strictEqual(annotations[0].timestamp, 12);

      assert.isTrue(annotations[1] instanceof tr.model.RectAnnotation);
      assert.deepEqual(annotations[1].startLocation, location1);
      assert.deepEqual(annotations[1].endLocation, location2);

      assert.isTrue(
          annotations[2] instanceof tr.model.CommentBoxAnnotation);
      assert.strictEqual(annotations[2].text, 'test');
      assert.deepEqual(annotations[2].location, location1);
    }

    const eventData = { traceEvents: [
      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'}],
    traceAnnotations: [
      {typeName: 'xmarker', args: {timestamp: 12}},
      {typeName: 'rect', args: {
        start: location1.toDict(), end: location2.toDict()}},
      {typeName: 'comment_box', args: {text: 'test',
        location: location1.toDict()}}
    ]};
    checkParsedAndStreamInput(eventData, checkModel);
  });

  test('importDisplayTimeUnit', function() {
    function checkModel(m) {
      assert.strictEqual(m.intrinsicTimeUnit, tr.b.TimeDisplayModes.ns);
    }

    const eventData = {
      traceEvents: [
        {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'}],
      displayTimeUnit: 'ns'
    };
    checkParsedAndStreamInput(eventData, checkModel);
    const m = makeModel(JSON.stringify(eventData));
    checkModel(m);
  });

  test('extractBattorSubTraces', function() {
    const battorLog = '# BattOr\n# voltage range [0.000000, 7196.484161] mV\n' +
        '#current range [11.898481, 2110.900916] mA\n' +
        '# sample_rate=10000Hz, gain=30.257143x\n' +
        '# filpot_pos=3, amppot_pos=35, timer_ovf=399, timer_div=4 ovs_bits=0\n' + // @suppress longLineCheck
        '0.0 1040.3 3984.2\n' +
        '0.1 1081.3 3987.8\n' +
        '0.2 1092.6 3987.8\n' +
        '0.3 1070.0 3987.8\n' +
        '0.4 1017.7 3994.8\n';

    const eventData = {
      traceEvents: [
        { name: 'a', args: {}, pid: 1, ts: 0, cat: 'baz', tid: 2, ph: 'B', sf: 7 }, // @suppress longLineCheck
        { name: 'b', args: {}, pid: 1, ts: 5, cat: 'baz', tid: 2, ph: 'E', sf: 8 } // @suppress longLineCheck
      ],
      powerTraceAsString: battorLog
    };
    let m = new tr.Model();
    let importer = new tr.e.importer.TraceEventImporter(m, eventData);
    let subTraces = importer.extractSubtraces();
    assert.isTrue(subTraces instanceof Array);
    assert.strictEqual(subTraces.length, 1);
    assert.strictEqual(subTraces[0], battorLog);

    m = new tr.Model();
    const stream = new tr.b.InMemoryTraceStream(
        stringToUint8Array(JSON.stringify(eventData)), false);
    importer = new tr.e.importer.TraceEventImporter(m, stream);
    subTraces = importer.extractSubtraces();
    assert.isTrue(subTraces instanceof Array);
    assert.strictEqual(subTraces.length, 1);
    assert.strictEqual(subTraces[0], battorLog);
  });

  test('metadataParsing', function() {
    const metadataValue = {value: {}};

    function checkModel(m) {
      assert.isTrue(m.metadata instanceof Array);
      assert.strictEqual(m.metadata.length, 1);
      assert.strictEqual(m.metadata[0].name, 'metadata');
      assert.deepEqual(m.metadata[0].value, metadataValue);
    }

    const eventData = {
      traceEvents: [
        { name: 'a', args: {}, pid: 1, ts: 0, cat: 'baz', tid: 2, ph: 'B', sf: 7 }, // @suppress longLineCheck
        { name: 'b', args: {}, pid: 1, ts: 5, cat: 'baz', tid: 2, ph: 'E', sf: 8 } // @suppress longLineCheck
      ],
      metadata: metadataValue
    };
    checkParsedAndStreamInput(eventData, checkModel);
  });

  test('importTraceConfigAsMetadata', function() {
    const traceConfigData = {
      enable_argument_filter: false,
      enable_sampling: false,
      enable_systrace: false,
      record_mode: 'record-until-full'
    };

    function checkModel(m) {
      assert.isTrue(m.metadata instanceof Array);
      assert.strictEqual(m.metadata.length, 1);
      assert.strictEqual(m.metadata[0].name, 'TraceConfig');
      assert.deepEqual(m.metadata[0].value, traceConfigData);
    }

    const eventData = {
      traceEvents: [
        { name: 'TraceConfig', args: { value: traceConfigData },
          pid: 2, ts: 0, tid: 1, ph: 'M', cat: '__metadata' },
      ],
    };
    checkParsedAndStreamInput(eventData, checkModel);
  });

  test('importMarks', function() {
    function checkModel(m) {
      const p = m.processes[1];
      const t = p.threads[2];
      assert.lengthOf(t.sliceGroup.slices, 2);

      const s0 = t.sliceGroup.slices[0];
      assert.strictEqual(s0.title, 'mark1');
      assert.strictEqual(s0.start, 0);

      const s1 = t.sliceGroup.slices[1];
      assert.strictEqual(s1.title, 'mark2');
      assert.strictEqual(s1.start, .01);
    }

    const eventData = {
      traceEvents: [
        { name: 'mark1', args: {}, pid: 1, tid: 2, ts: 0, tts: 0, cat: 'blink.user_timing', ph: 'R' }, // @suppress longLineCheck
        { name: 'mark2', args: {}, pid: 1, tid: 2, ts: 10, tts: 10, cat: 'blink.user_timing', ph: 'R' } // @suppress longLineCheck
      ]
    };
    checkParsedAndStreamInput(eventData, checkModel);
  });

  test('createNestableAsyncSlicesForUserTimingWithoutArgs', function() {
    function checkModel(m) {
      assert(m.numProcesses, 1);
      const p = m.processes[1];
      assert.isDefined(p);
      assert.strictEqual(p.numThreads, 1);
      const t = p.threads[2];
      const asyncSliceGroup = t.asyncSliceGroup;
      assert.strictEqual(asyncSliceGroup.length, 2);
      for (let i = 0; i < asyncSliceGroup.length; ++i) {
        assert.isTrue(asyncSliceGroup.slices[i] instanceof MeasureAsyncSlice);
      }

      const groupA = asyncSliceGroup.slices[0];
      assert.strictEqual(groupA.viewSubGroupTitle, 'A');
      assert.strictEqual(groupA.title, 'a1');
      assert.strictEqual(groupA.subSlices.length, 0);
      const groupB = asyncSliceGroup.slices[1];
      assert.strictEqual(groupB.viewSubGroupTitle, 'B');
      assert.strictEqual(groupB.title, 'b1');
      assert.strictEqual(groupB.subSlices.length, 2);
      const groupBSubSlice1 = groupB.subSlices[0];
      assert.strictEqual(groupBSubSlice1.viewSubGroupTitle, 'B');
      assert.strictEqual(groupBSubSlice1.title, 'b2');
      assert.strictEqual(groupBSubSlice1.subSlices.length, 1);
      assert.strictEqual(groupBSubSlice1.subSlices[0].viewSubGroupTitle, 'B');
      assert.strictEqual(groupBSubSlice1.subSlices[0].title, 'b3');
      assert.strictEqual(groupBSubSlice1.subSlices[0].subSlices.length, 0);
      const groupBSubSlice2 = groupB.subSlices[1];
      assert.strictEqual(groupBSubSlice2.viewSubGroupTitle, 'B');
      assert.strictEqual(groupBSubSlice2.title, 'b4');
      assert.strictEqual(groupBSubSlice2.subSlices.length, 0);
    }

    /**
     * Structure of this async slices
     *
     * Group A:
     *
     * |__________|
     *      a1
     *
     * Group B:
     *
     *                                |______________________________|
     *                                               b1
     *                                           |__________||_|
     *                                                b2      b4
     *                                            |_|
     *                                             b3
     **/
    const events = [
      {
        name: 'A:a1',
        args: {params: ''},
        pid: 1,
        ts: 100,
        cat: 'blink.user_timing',
        tid: 2,
        id: 3,
        ph: 'b'
      },
      {
        name: 'A:a1',
        args: {params: ''},
        pid: 1,
        ts: 110,
        cat: 'blink.user_timing',
        tid: 2,
        id: 3,
        ph: 'e'
      },
      {
        name: 'B:b1',
        args: {params: ''},
        pid: 1,
        ts: 120,
        cat: 'blink.user_timing',
        tid: 2,
        id: 4,
        ph: 'b'
      },
      {
        name: 'B:b2',
        args: {params: ''},
        pid: 1,
        ts: 130,
        cat: 'blink.user_timing',
        tid: 2,
        id: 5,
        ph: 'b'
      },
      {
        name: 'B:b3',
        args: {params: ''},
        pid: 1,
        ts: 131,
        cat: 'blink.user_timing',
        tid: 2,
        id: 5,
        ph: 'b'
      },
      {
        name: 'B:b3',
        args: {params: ''},
        pid: 1,
        ts: 132,
        cat: 'blink.user_timing',
        tid: 2,
        id: 5,
        ph: 'e'
      },
      {
        name: 'B:b2',
        args: {params: ''},
        pid: 1,
        ts: 140,
        cat: 'blink.user_timing',
        tid: 2,
        id: 5,
        ph: 'e'
      },
      {
        name: 'B:b4',
        args: {params: ''},
        pid: 1,
        ts: 141,
        cat: 'blink.user_timing',
        tid: 2,
        id: 5,
        ph: 'b'
      },
      {
        name: 'B:b4',
        args: {params: ''},
        pid: 1,
        ts: 142,
        cat: 'blink.user_timing',
        tid: 2,
        id: 5,
        ph: 'e'
      },
      {
        name: 'B:b1',
        args: {params: ''},
        pid: 1,
        ts: 150,
        cat: 'blink.user_timing',
        tid: 2,
        id: 4,
        ph: 'e'
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('createNestableAsyncSlicesForUserTimingWithArgs', function() {
    function checkModel(m) {
      assert(m.numProcesses, 1);
      const p = m.processes[1];
      assert.isDefined(p);
      assert.strictEqual(p.numThreads, 1);
      const t = p.threads[2];
      const asyncSliceGroup = t.asyncSliceGroup;
      assert.strictEqual(asyncSliceGroup.length, 2);
      for (let i = 0; i < asyncSliceGroup.length; ++i) {
        assert.isTrue(asyncSliceGroup.slices[i] instanceof MeasureAsyncSlice);
      }

      const a1 = asyncSliceGroup.slices[0];
      assert.strictEqual(a1.viewSubGroupTitle, 'A');
      assert.strictEqual(a1.title, 'a1');
      assert.strictEqual(a1.subSlices.length, 0);
      assert.deepEqual(a1.args, {a: 1});
      const a2 = asyncSliceGroup.slices[1];
      assert.strictEqual(a2.viewSubGroupTitle, 'A');
      assert.strictEqual(a2.title, 'a2');
      assert.strictEqual(a2.subSlices.length, 0);
      assert.deepEqual(a2.args, {a: 2, b: 2});
    }

    /**
     * Structure of this async slices
     *
     * Group A:
     *
     * |__________|          |__________|
     *      a1                    a2
     *
     * a1.args = {a: 1}
     * a2.args = {a: 2, b: 2}
     **/
    const events = [
      {
        name: 'A:a1/eyJhIjoxfQ==',
        args: {params: ''},
        pid: 1,
        ts: 100,
        cat: 'blink.user_timing',
        tid: 2,
        id: 3,
        ph: 'b'
      },
      {
        name: 'A:a1/eyJhIjoxfQ==',
        args: {params: ''},
        pid: 1,
        ts: 110,
        cat: 'blink.user_timing',
        tid: 2,
        id: 3,
        ph: 'e'
      },
      {
        name: 'A:a2/eyJhIjoyLCJiIjoyfQ==',
        args: {params: ''},
        pid: 1,
        ts: 120,
        cat: 'blink.user_timing',
        tid: 2,
        id: 4,
        ph: 'b'
      },
      {
        name: 'A:a2/eyJhIjoyLCJiIjoyfQ==',
        args: {params: ''},
        pid: 1,
        ts: 130,
        cat: 'blink.user_timing',
        tid: 2,
        id: 4,
        ph: 'e'
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('UserTimingAsyncSlicesWithNormalAsyncSlices', function() {
    function checkModel(m) {
      assert(m.numProcesses, 1);
      const p = m.processes[1];
      assert.isDefined(p);
      assert.strictEqual(p.numThreads, 1);
      const t = p.threads[2];
      const asyncSliceGroup = t.asyncSliceGroup;
      assert.strictEqual(asyncSliceGroup.length, 3);
      assert.isTrue(asyncSliceGroup.slices[0] instanceof MeasureAsyncSlice);
      assert.isFalse(asyncSliceGroup.slices[1] instanceof MeasureAsyncSlice);
      assert.isFalse(asyncSliceGroup.slices[2] instanceof MeasureAsyncSlice);

      const a1 = asyncSliceGroup.slices[0];
      assert.strictEqual(a1.viewSubGroupTitle, 'A');
      assert.strictEqual(a1.title, 'a1');
      assert.strictEqual(a1.subSlices.length, 1);
      const a2 = a1.subSlices[0];
      assert.strictEqual(a2.viewSubGroupTitle, 'A');
      assert.strictEqual(a2.title, 'a2');
      assert.strictEqual(a2.subSlices.length, 0);
      const B = asyncSliceGroup.slices[1];
      assert.strictEqual(B.viewSubGroupTitle, 'B');
      assert.strictEqual(B.title, 'B');
      assert.strictEqual(B.subSlices.length, 0);
      const C = asyncSliceGroup.slices[2];
      assert.strictEqual(C.viewSubGroupTitle, 'C');
      assert.strictEqual(C.title, 'C');
      assert.strictEqual(C.subSlices.length, 0);
    }

    /**
     * Structure of user timing async slices
     *
     * Group A:
     *
     * |__________|
     *      a1
     *  |__|
     *   a2
     *
     * B
     *         |__|
     *          B
     * C
     *             |_|
     *              C
     **/
    const events = [
      {
        name: 'A:a1', args: {params: ''}, pid: 1, ts: 1,
        cat: 'blink.user_timing', tid: 2, id: 3, ph: 'b'
      },
      {
        name: 'A:a1', args: {params: ''}, pid: 1, ts: 11,
        cat: 'blink.user_timing', tid: 2, id: 3, ph: 'e'
      },
      {
        name: 'A:a2', args: {params: ''}, pid: 1, ts: 2,
        cat: 'blink.user_timing', tid: 2, id: 4, ph: 'b'
      },
      {
        name: 'A:a2', args: {params: ''}, pid: 1, ts: 4,
        cat: 'blink.user_timing', tid: 2, id: 4, ph: 'e'
      },
      {
        name: 'B', args: {}, pid: 1, ts: 9, cat: 'foo',
        tid: 2, ph: 'b', id: 5
      },
      {
        name: 'B', args: {}, pid: 1, ts: 11, cat: 'foo',
        tid: 2, ph: 'e', id: 5
      },
      {
        name: 'C', args: {}, pid: 1, ts: 12, cat: 'foo',
        tid: 2, ph: 'b', id: 6
      },
      {
        name: 'C', args: {}, pid: 1, ts: 13, cat: 'foo',
        tid: 2, ph: 'e', id: 6
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('UserTimingAsyncSlicesParsingMeasureNameShouldNotFail', function() {
    function checkModel(m) {
      assert(m.numProcesses, 1);
      const p = m.processes[1];
      assert.isDefined(p);
      assert.strictEqual(p.numThreads, 1);
      const t = p.threads[2];
      const asyncSliceGroup = t.asyncSliceGroup;

      assert.strictEqual(asyncSliceGroup.length, 1);
      assert.isTrue(asyncSliceGroup.slices[0] instanceof MeasureAsyncSlice);

      const slice = asyncSliceGroup.slices[0];
      assert.strictEqual(slice.viewSubGroupTitle, '<extended@component');
      assert.strictEqual(slice.title,
          'shared@dropdown/dropdown-options::ember30> (Rendering: initial)');
      assert.strictEqual(slice.subSlices.length, 3);
      assert.strictEqual(slice.subSlices[0].title,
          'shared@dropdown/dropdown-options::ember30>:didReceiveAttrs:34');
      assert.strictEqual(slice.subSlices[1].title,
          'shared@dropdown/dropdown-options::ember30>:willRender:35');
      assert.strictEqual(slice.subSlices[2].title,
          'shared@dropdown/dropdown-options::ember30>:willInsertElement:36');
    }

    // events from ember-perf-timeline addon performance.measure() calls
    const events = [
      {
        pid: 1, tid: 2, ts: 2695561803887, ph: 'b', cat: 'blink.user_timing',
        name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30>:didReceiveAttrs:34', // @suppress longLineCheck
        args: {}, tts: 10533196, id: '0x9e9a4a'
      },
      {
        pid: 1, tid: 2, ts: 2695561803907, ph: 'e', cat: 'blink.user_timing',
        name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30>:didReceiveAttrs:34', // @suppress longLineCheck
        args: {}, tts: 10533202, id: '0x9e9a4a'
      },
      {
        pid: 1, tid: 2, ts: 2695561803987, ph: 'b', cat: 'blink.user_timing',
        name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30>:willRender:35', // @suppress longLineCheck
        args: {}, tts: 10533295, id: '0xd0fc77'
      },
      {
        pid: 1, tid: 2, ts: 2695561804002, ph: 'e', cat: 'blink.user_timing',
        name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30>:willRender:35', // @suppress longLineCheck
        args: {}, tts: 10533300, id: '0xd0fc77'
      },
      {
        pid: 1, tid: 2, ts: 2695561804252, ph: 'b', cat: 'blink.user_timing',
        name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30>:willInsertElement:36', // @suppress longLineCheck
        args: {}, tts: 10533557, id: '0x5dfb0b'
      },
      {
        pid: 1, tid: 2, ts: 2695561804262, ph: 'e', cat: 'blink.user_timing',
        name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30>:willInsertElement:36', // @suppress longLineCheck
        args: {}, tts: 10533561, id: '0x5dfb0b'
      },
      {
        pid: 1, tid: 2, ts: 2695561803852, ph: 'b', cat: 'blink.user_timing',
        name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30> (Rendering: initial)', // @suppress longLineCheck
        args: {}, tts: 10533951, id: '0xc824e5'
      },
      {
        pid: 1, tid: 2, ts: 2695561804652, ph: 'e', cat: 'blink.user_timing',
        name: '\u003Cextended@component:shared@dropdown/dropdown-options::ember30> (Rendering: initial)', // @suppress longLineCheck
        args: {}, tts: 10533956, id: '0xc824e5'
      }
    ];
    checkParsedAndStreamInput(events, checkModel);
  });

  test('clockSync_requesterWithoutMetadata', function() {
    const events = [{
      name: 'clock_sync', args: {sync_id: 'abc', issue_ts: 5000},
      pid: 1, ts: 15000, cat: '__metadata', tid: 2, ph: 'c'
    }];

    const io = new tr.importer.ImportOptions();
    const m = new tr.Model();
    const i = new tr.importer.Import(m, io);

    m.clockSyncManager.addClockSyncMarker(
        ClockDomainId.LINUX_FTRACE_GLOBAL, 'abc', 15);

    i.importTraces([events]);

    // The clock sync happened in UNKNOWN_CHROME_LEGACY at 10ms and in
    // LINUX_FTRACE_GLOBAL at 15ms. The transformer should shift the ftrace
    // timestamps back by 5ms.
    assert.strictEqual(
        m.clockSyncManager.getModelTimeTransformer(
            ClockDomainId.UNKNOWN_CHROME_LEGACY)(5),
        5);
    assert.strictEqual(
        m.clockSyncManager.getModelTimeTransformer(
            ClockDomainId.LINUX_FTRACE_GLOBAL)(15),
        10);
    assert.isFalse(m.hasImportWarnings);
  });

  test('clockSync_requesterWithoutMetadata_stream', function() {
    const events = [{
      name: 'clock_sync', args: {sync_id: 'abc', issue_ts: 5000},
      pid: 1, ts: 15000, cat: '__metadata', tid: 2, ph: 'c'
    }];
    const stream = new tr.b.InMemoryTraceStream(
        stringToUint8Array(JSON.stringify(events)), false);

    const io = new tr.importer.ImportOptions();
    const m = new tr.Model();
    const i = new tr.importer.Import(m, io);

    m.clockSyncManager.addClockSyncMarker(
        ClockDomainId.LINUX_FTRACE_GLOBAL, 'abc', 15);

    i.importTraces([stream]);

    // The clock sync happened in UNKNOWN_CHROME_LEGACY at 10ms and in
    // LINUX_FTRACE_GLOBAL at 15ms. The transformer should shift the ftrace
    // timestamps back by 5ms.
    assert.strictEqual(
        m.clockSyncManager.getModelTimeTransformer(
            ClockDomainId.UNKNOWN_CHROME_LEGACY)(5),
        5);
    assert.strictEqual(
        m.clockSyncManager.getModelTimeTransformer(
            ClockDomainId.LINUX_FTRACE_GLOBAL)(15),
        10);
    assert.isFalse(m.hasImportWarnings);
  });


  test('clockSync_requesterWithMetadata', function() {
    function checkModel(m) {
      // Make sure that our clock domain was chosen as the model.
      assert.strictEqual(
          m.clockSyncManager
              .getModelTimeTransformer(ClockDomainId.BATTOR)(.005),
          .005);
      assert.isFalse(m.hasImportWarnings);
    }

    const events = {
      traceEvents: [{
        name: 'clock_sync', args: {sync_id: 'abc', issue_ts: 5},
        pid: 1, ts: 15, cat: '__metadata', tid: 2, ph: 'c'
      }],
      metadata: {
        'clock-domain': ClockDomainId.BATTOR
      }
    };
    checkParsedAndStreamInput(events, checkModel);
  });

  test('clockSync_recipient', function() {
    function checkModel(m) {
      m.clockSyncManager.addClockSyncMarker(
          ClockDomainId.LINUX_CLOCK_MONOTONIC, 'abc', 15);

      assert.strictEqual(
          m.clockSyncManager.getModelTimeTransformer(
              ClockDomainId.TELEMETRY)(10),
          10);
      assert.strictEqual(
          m.clockSyncManager.getModelTimeTransformer(
              ClockDomainId.LINUX_CLOCK_MONOTONIC)(10),
          10);
    }

    const events = {
      traceEvents: [{
        name: 'clock_sync', args: {sync_id: 'abc'}, pid: 1, ts: 15000,
        cat: '__metadata', tid: 2, ph: 'c'
      }],
      metadata: {
        'clock-domain': ClockDomainId.TELEMETRY
      }
    };
    checkParsedAndStreamInput(events, checkModel);
  });

  test('clockSync_missingSyncId', function() {
    const events = [{
      name: 'clock_sync', args: {issue_ts: 5},
      pid: 1, ts: 15, cat: '__metadata', tid: 2, ph: 'c'
    }];
    checkParsedAndStreamInput(events, m => assert.isTrue(m.hasImportWarnings));
  });

  test('clockSync_legacyChrome', function() {
    function checkModel(m) {
      m.clockSyncManager.addClockSyncMarker(
          ClockDomainId.TELEMETRY, 'abc', 15);

      assert.strictEqual(
          m.clockSyncManager.getModelTimeTransformer(
              ClockDomainId.UNKNOWN_CHROME_LEGACY)(10),
          10);
      assert.strictEqual(
          m.clockSyncManager.getModelTimeTransformer(
              ClockDomainId.TELEMETRY)(15),
          10);
    }

    const events = {
      traceEvents: [
        {
          name: 'ClockSyncEvent.abc', ts: 5000,
          cat: 'blink.user_timing', pid: 1, tid: 2, ph: 'S'
        },
        {
          name: 'ClockSyncEvent.abc', ts: 15000,
          cat: 'blink.user_timing', pid: 1, tid: 2, ph: 'F'
        }
      ]
    };
    checkParsedAndStreamInput(events, checkModel);
  });

  test('clockSync_legacyChromeThrowsWithInconsistentSyncID', function() {
    const events = {
      traceEvents: [
        {
          name: 'ClockSyncEvent.abc', ts: 5000,
          cat: 'blink.user_timing', pid: 1, tid: 2, ph: 'S'
        },
        {
          name: 'ClockSyncEvent.def', ts: 15000,
          cat: 'blink.user_timing', pid: 1, tid: 2, ph: 'F'
        }
      ]
    };

    assert.throws(() => makeModel(events),
        'Inconsistent clock sync ID of legacy Chrome clock sync events');
    // TODO(chiniforooshan): When the input is a trace stream, it is processed
    // using the Oboe.js library. Oboe.js wraps exceptions thrown from callbacks
    // and uses setTimeout to throw them in another event loop so that stream
    // parsing is not interrupted.
    //
    // The importer's behaviour should be the same for different types of the
    // same input. We should either modify Oboe.js to throw right away, or wrap
    // exceptions in the importer, too.
  });

  test('importV8SamplesInStreamingFormat', function() {
    function checkModel(m) {
      const samples = m.samples;
      assert.strictEqual(m.samples.length, 4);
      assert.deepEqual(samples.map(sample => sample.start.toFixed(3)), [
        '0.011',
        '0.012',
        '0.015',
        '0.018'
      ]);
      assert.deepEqual(samples[0].userFriendlyStack, [
        'b url: b.js:1:2 Deoptimized reason: a reason',
        'a url: a.js:1:2'
      ]);
      assert.deepEqual(samples[1].userFriendlyStack, [
        'a url: a.js:1:2'
      ]);
      assert.deepEqual(samples[2].userFriendlyStack, [
        'd url: d.js:2:3 Deoptimized reason: another reason',
        'b url: b.js:1:2 Deoptimized reason: a reason',
        'a url: a.js:1:2'
      ]);
      assert.deepEqual(samples[3].userFriendlyStack, [
        'c url: c.js:2:3',
        'a url: a.js:1:2'
      ]);
    }

    const events = {
      'traceEvents': [
        {'pid': 1, 'tid': 2, 'ts': 3, 'ph': 'P',
          'cat': 'disabled-by-default-v8.cpu_profiler',
          'name': 'Profile',
          'args': {'data': {'startTime': 10}},
          'id': 123
        },
        {'pid': 1, 'tid': 3, 'ts': 4, 'ph': 'P',
          'cat': 'disabled-by-default-v8.cpu_profiler',
          'name': 'Profile',
          'args': {
            'data': {
              'timeDeltas': [1, 1],
              'cpuProfile': {
                'nodes': [
                  // eslint-disable-next-line
                 {'callFrame': {'functionName': '(root)', 'scriptId': 0}, 'id': 1},
                  // eslint-disable-next-line
                 {'callFrame': {'functionName': 'a', 'url': 'a.js', 'scriptId': 1, 'lineNumber': 1, 'columnNumber': 2}, 'id': 2, 'parent': 1},
                  // eslint-disable-next-line
                 {'callFrame': {'functionName': 'b', 'url': 'b.js', 'scriptId': 2, 'lineNumber': 1, 'columnNumber': 2}, 'deoptReason': 'a reason', 'id': 3, 'parent': 2}
                ],
                'samples': [3, 2]
              }
            }
          },
          'id': 123
        },
        {'pid': 1, 'tid': 3, 'ts': 4, 'ph': 'P',
          'cat': 'disabled-by-default-v8.cpu_profiler',
          'name': 'Profile',
          'args': {
            'data': {
              'timeDeltas': [3, 3],
              'cpuProfile': {
                'nodes': [
                  // eslint-disable-next-line
                 {'callFrame': {'functionName': 'c', 'url': 'c.js', 'scriptId': 3, 'lineNumber': 2, 'columnNumber': 3}, 'id': 4, 'parent': 2},
                  // eslint-disable-next-line
                 {'callFrame': {'functionName': 'd', 'url': 'd.js', 'scriptId': 4, 'lineNumber': 2, 'columnNumber': 3}, 'deoptReason': 'another reason', 'id': 5, 'parent': 3}
                ],
                'samples': [5, 4]
              }
            }
          },
          'id': 123
        }
      ]
    };
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('importV8SamplesInStreamingFormat_id_collision', function() {
    function checkModel(m) {
      const samples = m.samples;
      assert.strictEqual(m.samples.length, 4);
      assert.strictEqual(Object.keys(m.processes).length, 2);
      assert.deepEqual(samples.map(sample => sample.start.toFixed(3)), [
        '0.011',
        '0.012',
        '0.013',
        '0.016'
      ]);
      assert.deepEqual(samples.map(sample => sample.thread.getProcess().pid), [
        1,
        1,
        2,
        2
      ]);
      assert.deepEqual(samples[0].userFriendlyStack, [
        'b url: b.js:1:2 Deoptimized reason: a reason',
        'a url: a.js:1:2'
      ]);
      assert.deepEqual(samples[1].userFriendlyStack, [
        'a url: a.js:1:2'
      ]);
      assert.deepEqual(samples[2].userFriendlyStack, [
        'd url: d.js:2:3 Deoptimized reason: another reason',
      ]);
      assert.deepEqual(samples[3].userFriendlyStack, [
        'c url: c.js:2:3',
      ]);
    }

    const events = {
      'traceEvents': [
        {'pid': 1, 'tid': 2, 'ts': 3, 'ph': 'P',
          'cat': 'disabled-by-default-v8.cpu_profiler',
          'name': 'Profile',
          'args': {'data': {'startTime': 10}},
          'id': 123
        },
        {'pid': 2, 'tid': 2, 'ts': 3, 'ph': 'P',
          'cat': 'disabled-by-default-v8.cpu_profiler',
          'name': 'Profile',
          'args': {'data': {'startTime': 10}},
          'id': 123
        },
        {'pid': 1, 'tid': 3, 'ts': 4, 'ph': 'P',
          'cat': 'disabled-by-default-v8.cpu_profiler',
          'name': 'Profile',
          'args': {
            'data': {
              'timeDeltas': [1, 1],
              'cpuProfile': {
                'nodes': [
                  // eslint-disable-next-line
                 {'callFrame': {'functionName': '(root)', 'scriptId': 0}, 'id': 1},
                  // eslint-disable-next-line
                 {'callFrame': {'functionName': 'a', 'url': 'a.js', 'scriptId': 1, 'lineNumber': 1, 'columnNumber': 2}, 'id': 2, 'parent': 1},
                  // eslint-disable-next-line
                 {'callFrame': {'functionName': 'b', 'url': 'b.js', 'scriptId': 2, 'lineNumber': 1, 'columnNumber': 2}, 'deoptReason': 'a reason', 'id': 3, 'parent': 2}
                ],
                'samples': [3, 2]
              }
            }
          },
          'id': 123
        },
        {'pid': 2, 'tid': 3, 'ts': 4, 'ph': 'P',
          'cat': 'disabled-by-default-v8.cpu_profiler',
          'name': 'Profile',
          'args': {
            'data': {
              'timeDeltas': [3, 3],
              'cpuProfile': {
                'nodes': [
                  // eslint-disable-next-line
                 {'callFrame': {'functionName': 'c', 'url': 'c.js', 'scriptId': 3, 'lineNumber': 2, 'columnNumber': 3}, 'id': 4, 'parent': 2},
                  // eslint-disable-next-line
                 {'callFrame': {'functionName': 'd', 'url': 'd.js', 'scriptId': 4, 'lineNumber': 2, 'columnNumber': 3}, 'deoptReason': 'another reason', 'id': 5, 'parent': 3}
                ],
                'samples': [5, 4]
              }
            }
          },
          'id': 123
        }
      ]
    };
    checkParsedAndStreamInput(events, checkModel, false);
  });

  test('scopedIdForEvent_defaultScopeAsyncEvent', function() {
    const event = {pid: 1, ph: 'b', id: '0x1000'};
    const id = tr.e.importer.TraceEventImporter.scopedIdForEvent_(event);
    assert.isDefined(id);
    assert.strictEqual('0x1000', id.id);
    assert.strictEqual('ptr', id.scope);
    assert.isUndefined(id.pid);
  });

  test('scopedIdForEvent_defaultScopeContextEvent', function() {
    const event = {pid: 1, ph: '(', id: '0x1000'};
    const id = tr.e.importer.TraceEventImporter.scopedIdForEvent_(event);
    assert.isDefined(id);
    assert.strictEqual('0x1000', id.id);
    assert.strictEqual('ptr', id.scope);
    assert.strictEqual(1, id.pid);
  });

  test('scopedIdForEvent_defaultScopeProcessLocalId', function() {
    const event = {pid: 1, ph: 'b', id2: {local: '0x1000'}};
    const id = tr.e.importer.TraceEventImporter.scopedIdForEvent_(event);
    assert.isDefined(id);
    assert.strictEqual('0x1000', id.id);
    assert.strictEqual('ptr', id.scope);
    assert.strictEqual(1, id.pid);
  });

  test('scopedIdForEvent_defaultScopeGlobalId', function() {
    const event = {pid: 1, ph: '(', id2: {global: '0x1000'}};
    const id = tr.e.importer.TraceEventImporter.scopedIdForEvent_(event);
    assert.isDefined(id);
    assert.strictEqual('0x1000', id.id);
    assert.strictEqual('ptr', id.scope);
    assert.isUndefined(id.pid);
  });

  test('scopedIdForEvent_explicitScopeGlobalId', function() {
    const event = {pid: 1, ph: '(', id2: {global: '0x1000'}, scope: 'scope'};
    const id = tr.e.importer.TraceEventImporter.scopedIdForEvent_(event);
    assert.isDefined(id);
    assert.strictEqual('0x1000', id.id);
    assert.strictEqual('scope', id.scope);
    assert.isUndefined(id.pid);
  });

  // TODO(nduca): one slice, two threads
  // TODO(nduca): one slice, two pids
});
</script>
